diff --git a/internal/staging/primitives/README.md b/internal/staging/primitives/README.md new file mode 100644 index 0000000000..e9e3784bb3 --- /dev/null +++ b/internal/staging/primitives/README.md @@ -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. diff --git a/internal/staging/primitives/error.go b/internal/staging/primitives/error.go new file mode 100644 index 0000000000..00fa112387 --- /dev/null +++ b/internal/staging/primitives/error.go @@ -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} +} diff --git a/internal/staging/primitives/error_test.go b/internal/staging/primitives/error_test.go new file mode 100644 index 0000000000..043833c3ec --- /dev/null +++ b/internal/staging/primitives/error_test.go @@ -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 + } + } +} diff --git a/internal/staging/primitives/pow.go b/internal/staging/primitives/pow.go new file mode 100644 index 0000000000..e730e12da9 --- /dev/null +++ b/internal/staging/primitives/pow.go @@ -0,0 +1,261 @@ +// 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 ( + "fmt" + + "github.com/decred/dcrd/chaincfg/chainhash" + "github.com/decred/dcrd/internal/staging/primitives/uint256" +) + +// DiffBitsToUint256 converts the compact representation used to encode +// difficulty targets to an unsigned 256-bit integer. The representation is +// similar to IEEE754 floating point numbers. +// +// Like IEEE754 floating point, there are three basic components: the sign, +// the exponent, and the mantissa. They are broken out as follows: +// +// * the most significant 8 bits represent the unsigned base 256 exponent +// * bit 23 (the 24th bit) represents the sign bit +// * the least significant 23 bits represent the mantissa +// +// ------------------------------------------------- +// | Exponent | Sign | Mantissa | +// ------------------------------------------------- +// | 8 bits [31-24] | 1 bit [23] | 23 bits [22-00] | +// ------------------------------------------------- +// +// The formula to calculate N is: +// N = (-1^sign) * mantissa * 256^(exponent-3) +// +// Note that this encoding is capable of representing negative numbers as well +// as numbers much larger than the maximum value of an unsigned 256-bit integer. +// However, it is only used in Decred to encode unsigned 256-bit integers which +// represent difficulty targets, so rather than using a much less efficient +// arbitrary precision big integer, this implementation uses an unsigned 256-bit +// integer and returns flags to indicate whether or not the encoding was for a +// negative value and/or overflows a uint256 to enable proper error detection +// and stay consistent with legacy code. +func DiffBitsToUint256(bits uint32) (n uint256.Uint256, isNegative bool, overflows bool) { + // Extract the mantissa, sign bit, and exponent. + mantissa := bits & 0x007fffff + isSignBitSet := bits&0x00800000 != 0 + exponent := bits >> 24 + + // Nothing to do when the mantissa is zero as any multiple of it will + // necessarily also be 0 and therefore it can never be negative or overflow. + if mantissa == 0 { + return n, false, false + } + + // Since the base for the exponent is 256 = 2^8, the exponent is a multiple + // of 8 and thus the full 256-bit number is computed by shifting the + // mantissa right or left accordingly. + // + // This and the following section are equivalent to: + // + // N = mantissa * 256^(exponent-3) + if exponent <= 3 { + n.SetUint64(uint64(mantissa >> (8 * (3 - exponent)))) + return n, isSignBitSet, false + } + + // Notice that since this is decoding into a uint256, any values that take + // more than 256 bits to represent will overflow. Also, since the encoded + // exponent value is decreased by 3 and then multiplied by 8, any encoded + // exponent of 35 or greater will cause an overflow because 256/8 + 3 = 35. + // Decreasing the encoded exponent by one to 34 results in making 8 bits + // available, meaning the max mantissa can be 0xff in that case. Similarly, + // decreasing the encoded exponent again to 33 results in making a total of + // 16 bits available, meaning the max mantissa can be 0xffff in that case. + // Finally, decreasing the encoded exponent again to 32 results in making a + // total of 24 bits available and since the mantissa only encodes 23 bits, + // overflow is impossible for all encoded exponents of 32 or lower. + overflows = exponent >= 35 || (exponent >= 34 && mantissa > 0xff) || + (exponent >= 33 && mantissa > 0xffff) + if overflows { + return n, isSignBitSet, true + } + n.SetUint64(uint64(mantissa)) + n.Lsh(8 * (exponent - 3)) + return n, isSignBitSet, false +} + +// uint256ToDiffBits converts a uint256 to a compact representation using an +// unsigned 32-bit integer. The compact representation only provides 23 bits of +// precision, so values larger than (2^23 - 1) only encode the most significant +// digits of the number. See DiffBitsToUint256 for details. +// +// NOTE: The only difference between this function and the exported variant is +// that this one accepts a parameter to indicate whether or not the encoding +// should be for a negative value whereas the exported variant is always +// positive as expected for an unsigned value. This is done in order to stay +// consistent with the encoding used by legacy code and for testing purposes, +// however, difficulty bits are only used in Decred to encode unsigned 256-bit +// integers which represent difficulty targets, so, it will always be called +// with false in practice. +func uint256ToDiffBits(n *uint256.Uint256, isNegative bool) uint32 { + // No need to do any work if it's zero. + if n.IsZero() { + return 0 + } + + // Since the base for the exponent is 256, the exponent can be treated as + // the number of bytes it takes to represent the value. So, shift the + // number right or left accordingly. This is equivalent to: + // mantissa = n / 256^(exponent-3) + var mantissa uint32 + exponent := uint32((n.BitLen() + 7) / 8) + if exponent <= 3 { + mantissa = n.Uint32() << (8 * (3 - exponent)) + } else { + // Use a copy to avoid modifying the caller's original value. + mantissa = new(uint256.Uint256).RshVal(n, 8*(exponent-3)).Uint32() + } + + // When the mantissa already has the sign bit set, the number is too large + // to fit into the available 23-bits, so divide the number by 256 and + // increment the exponent accordingly. + if mantissa&0x00800000 != 0 { + mantissa >>= 8 + exponent++ + } + + // Pack the exponent, sign bit, and mantissa into an unsigned 32-bit int and + // return it. + // + // Note that the sign bit is conditionally set based on the provided flag + // since uint256s can never be negative. + bits := exponent<<24 | mantissa + if isNegative { + bits |= 0x00800000 + } + return bits +} + +// Uint256ToDiffBits converts a uint256 to a compact representation using an +// unsigned 32-bit integer. The compact representation only provides 23 bits of +// precision, so values larger than (2^23 - 1) only encode the most significant +// digits of the number. See DiffBitsToUint256 for details. +func Uint256ToDiffBits(n *uint256.Uint256) uint32 { + const isNegative = false + return uint256ToDiffBits(n, isNegative) +} + +// CalcWork calculates a work value from difficulty bits. Decred increases the +// difficulty for generating a block by decreasing the value which the generated +// hash must be less than. This difficulty target is stored in each block +// header using a compact representation as described in the documentation for +// DiffBitsToUint256. The main chain is selected by choosing the chain that has +// the most proof of work (highest difficulty). Since a lower target difficulty +// value equates to higher actual difficulty, the work value which will be +// accumulated must be the inverse of the difficulty. For legacy reasons, the +// result is zero when the difficulty is zero. Finally, to avoid really small +// floating point numbers, the result multiplies the numerator by 2^256 and adds +// 1 to the denominator. +func CalcWork(diffBits uint32) uint256.Uint256 { + // Return a work value of zero if the passed difficulty bits represent a + // negative number, a number that overflows a uint256, or zero. Note this + // should not happen in practice with valid blocks, but an invalid block + // could trigger it. + diff, isNegative, overflows := DiffBitsToUint256(diffBits) + if isNegative || overflows || diff.IsZero() { + return uint256.Uint256{} + } + + // The goal is to calculate 2^256 / (diff+1), where diff > 0 using a + // fixed-precision uint256. + // + // Since 2^256 can't be represented by a uint256, the calc is performed as + // follows: + // + // Notice: + // work = (2^256 / (diff+1)) + // => work = ((2^256-diff-1) / (diff+1))+1 + // + // Next, observe that 2^256-diff-1 is the one's complement of diff as a + // uint256 which is equivalent to the bitwise not. Also, of special note is + // the case when diff = 2^256-1 because (2^256-1)+1 ≡ 0 (mod 2^256) and + // thus would result in division by zero when working with a uint256. The + // original calculation would produce 1 in that case, so the resulting + // piecewise function is: + // + // {work = 1 , where diff = 2^256-1 + // {work = (^diff / (diff+1))+1, where 0 < diff < 2^256-1 + // + // However, a difficulty target of 2^256 - 1 is impossible to encode in the + // difficulty bits, so it is safe to ignore that case. + divisor := new(uint256.Uint256).SetUint64(1).Add(&diff) + return *diff.Not().Div(divisor).AddUint64(1) +} + +// HashToUint256 converts the provided hash to an unsigned 256-bit integer that +// can be used to perform math comparisons. +func HashToUint256(hash *chainhash.Hash) uint256.Uint256 { + // Hashes are a stream of bytes that do not have any inherent endianness to + // them, so they are interpreted as little endian for the purposes of + // 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 +} diff --git a/internal/staging/primitives/pow_bench_test.go b/internal/staging/primitives/pow_bench_test.go new file mode 100644 index 0000000000..176537e743 --- /dev/null +++ b/internal/staging/primitives/pow_bench_test.go @@ -0,0 +1,79 @@ +// 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 ( + "testing" + + "github.com/decred/dcrd/chaincfg/chainhash" +) + +// BenchmarkDiffBitsToUint256 benchmarks converting the compact representation +// used to encode difficulty targets to an unsigned 256-bit integer. +func BenchmarkDiffBitsToUint256(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + const input = 0x1b01330e + DiffBitsToUint256(input) + } +} + +// BenchmarkUint256ToDiffBits benchmarks converting an unsigned 256-bit integer +// to the compact representation used to encode difficulty targets. +func BenchmarkUint256ToDiffBits(b *testing.B) { + n := hexToUint256("1330e000000000000000000000000000000000000000000000000") + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + Uint256ToDiffBits(n) + } +} + +// BenchmarkCalcWork benchmarks calculating a work value from difficulty bits. +func BenchmarkCalcWork(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + const input = 0x1b01330e + CalcWork(input) + } +} + +// BenchmarkHashToUint256 benchmarks converting a hash to an unsigned 256-bit +// integer that can be used to perform math comparisons. +func BenchmarkHashToUint256(b *testing.B) { + h := "000000000000437482b6d47f82f374cde539440ddb108b0a76886f0d87d126b9" + hash, err := chainhash.NewHashFromStr(h) + if err != nil { + b.Fatalf("unexpected error: %v", err) + } + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + HashToUint256(hash) + } +} + +// BenchmarkCheckProofOfWork benchmarks ensuring a given block hash satisfies +// the proof of work requirements for given difficulty bits. +func BenchmarkCheckProofOfWork(b *testing.B) { + // Data from block 100k on the main network. + h := "00000000000004289d9a7b0f7a332fb60a1c221faae89a107ce3ab93eead2f93" + blockHash, err := chainhash.NewHashFromStr(h) + if err != nil { + b.Fatalf("unexpected error: %v", err) + } + const diffBits = 0x1a1194b4 + powLimit := hexToUint256(mockMainNetPowLimit()) + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = CheckProofOfWork(blockHash, diffBits, powLimit) + } +} diff --git a/internal/staging/primitives/pow_test.go b/internal/staging/primitives/pow_test.go new file mode 100644 index 0000000000..37d34bd7c8 --- /dev/null +++ b/internal/staging/primitives/pow_test.go @@ -0,0 +1,438 @@ +// 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 ( + "encoding/hex" + "errors" + "testing" + + "github.com/decred/dcrd/chaincfg/chainhash" + "github.com/decred/dcrd/internal/staging/primitives/uint256" +) + +// hexToBytes converts the passed hex string into bytes and will panic if there +// is an error. This is only provided for the hard-coded constants so errors in +// the source code can be detected. It will only (and must only) be called with +// hard-coded values. +func hexToBytes(s string) []byte { + b, err := hex.DecodeString(s) + if err != nil { + panic("invalid hex in source file: " + s) + } + return b +} + +// hexToUint256 converts the passed hex string into a Uint256 and will panic if +// there is an error. This is only provided for the hard-coded constants so +// errors in the source code can be detected. It will only (and must only) be +// called with hard-coded values. +func hexToUint256(s string) *uint256.Uint256 { + if len(s)%2 != 0 { + s = "0" + s + } + b := hexToBytes(s) + if len(b) > 32 { + panic("hex in source file overflows mod 2^256: " + s) + } + return new(uint256.Uint256).SetByteSlice(b) +} + +// TestDiffBitsToUint256 ensures converting from the compact representation used +// for target difficulties to unsigned 256-bit integers produces the correct +// results. +func TestDiffBitsToUint256(t *testing.T) { + t.Parallel() + + tests := []struct { + name string // test description + input uint32 // compact target difficulty bits to test + want string // expected uint256 + neg bool // expect result to be a negative number + overflows bool // expect result to overflow + }{{ + name: "mainnet block 1", + input: 0x1b01ffff, + want: "000000000001ffff000000000000000000000000000000000000000000000000", + }, { + name: "mainnet block 288", + input: 0x1b01330e, + want: "000000000001330e000000000000000000000000000000000000000000000000", + }, { + name: "higher diff (exponent 24, sign bit 0, mantissa 0x5fb28a)", + input: 0x185fb28a, + want: "00000000000000005fb28a000000000000000000000000000000000000000000", + }, { + name: "zero", + input: 0, + want: "00", + }, { + name: "-1 (exponent 1, sign bit 1, mantissa 0x10000)", + input: 0x1810000, + want: "01", + neg: true, + }, { + name: "-128 (exponent 2, sign bit 1, mantissa 0x08000)", + input: 0x2808000, + want: "80", + neg: true, + }, { + name: "-32768 (exponent 3, sign bit 1, mantissa 0x08000)", + input: 0x3808000, + want: "8000", + neg: true, + }, { + name: "-8388608 (exponent 4, sign bit 1, mantissa 0x08000)", + input: 0x4808000, + want: "800000", + neg: true, + }, { + name: "max uint256 + 1 via exponent 33 (overflows)", + input: 0x21010000, + want: "00", + neg: false, + overflows: true, + }, { + name: "negative max uint256 + 1 (negative and overflows)", + input: 0x21810000, + want: "00", + neg: true, + overflows: true, + }, { + name: "max uint256 + 1 via exponent 34 (overflows)", + input: 0x22000100, + want: "00", + neg: false, + overflows: true, + }, { + name: "max uint256 + 1 via exponent 35 (overflows)", + input: 0x23000001, + want: "00", + neg: false, + overflows: true, + }} + + for _, test := range tests { + want := hexToUint256(test.want) + + result, isNegative, overflows := DiffBitsToUint256(test.input) + if result.Cmp(want) != 0 { + t.Errorf("%q: mismatched result -- got %x, want %x", test.name, + result, want) + continue + } + if isNegative != test.neg { + t.Errorf("%q: mismatched negative -- got %v, want %v", test.name, + isNegative, test.neg) + continue + } + if overflows != test.overflows { + t.Errorf("%q: mismatched overflows -- got %v, want %v", test.name, + overflows, test.overflows) + continue + } + } +} + +// TestUint256ToDiffBits ensures converting from unsigned 256-bit integers to +// the representation used for target difficulties in the header bits field +// produces the correct results. +func TestUint256ToDiffBits(t *testing.T) { + t.Parallel() + + tests := []struct { + name string // test description + input string // uint256 to test + neg bool // treat as a negative number + want uint32 // expected encoded value + }{{ + name: "mainnet block 1", + input: "000000000001ffff000000000000000000000000000000000000000000000000", + want: 0x1b01ffff, + }, { + name: "mainnet block 288", + input: "000000000001330e000000000000000000000000000000000000000000000000", + want: 0x1b01330e, + }, { + name: "higher diff (exponent 24, sign bit 0, mantissa 0x5fb28a)", + input: "00000000000000005fb28a000000000000000000000000000000000000000000", + want: 0x185fb28a, + }, { + name: "zero", + input: "00", + want: 0, + }, { + name: "negative zero is zero", + input: "00", + neg: true, + want: 0, + }, { + name: "-1 (exponent 1, sign bit 1, mantissa 0x10000)", + input: "01", + neg: true, + want: 0x1810000, + }, { + name: "-128 (exponent 2, sign bit 1, mantissa 0x08000)", + input: "80", + neg: true, + want: 0x2808000, + }, { + name: "-32768 (exponent 3, sign bit 1, mantissa 0x08000)", + input: "8000", + neg: true, + want: 0x3808000, + }, { + name: "-8388608 (exponent 4, sign bit 1, mantissa 0x08000)", + input: "800000", + neg: true, + want: 0x4808000, + }} + + for _, test := range tests { + input := hexToUint256(test.input) + + // Either use the internal function or the exported function depending + // on whether or not the test is for negative inputs. This is done in + // order to ensure both funcs are tested and because only the internal + // one accepts a flag to specify the value should be treated as + // negative. + var result uint32 + if test.neg { + result = uint256ToDiffBits(input, true) + } else { + result = Uint256ToDiffBits(input) + } + if result != test.want { + t.Errorf("%q: mismatched result -- got %x, want %x", test.name, + result, test.want) + continue + } + } +} + +// TestCalcWork ensures calculating a work value from a compact target +// difficulty produces the correct results. +func TestCalcWork(t *testing.T) { + t.Parallel() + + tests := []struct { + name string // test description + input uint32 // target difficulty bits to test + want string // expected uint256 + }{{ + name: "mainnet block 1", + input: 0x1b01ffff, + want: "0000000000000000000000000000000000000000000000000000800040002000", + }, { + name: "mainnet block 288", + input: 0x1b01330e, + want: "0000000000000000000000000000000000000000000000000000d56f2dcbe105", + }, { + name: "higher diff (exponent 24)", + input: 0x185fb28a, + want: "000000000000000000000000000000000000000000000002acd33ddd458512da", + }, { + name: "zero", + input: 0, + want: "0000000000000000000000000000000000000000000000000000000000000000", + }, { + name: "max uint256", + input: 0x2100ffff, + want: "0000000000000000000000000000000000000000000000000000000000000001", + }, { + name: "negative target difficulty", + input: 0x1810000, + want: "0000000000000000000000000000000000000000000000000000000000000000", + }} + + for _, test := range tests { + want := hexToUint256(test.want) + result := CalcWork(test.input) + if !result.Eq(want) { + t.Errorf("%q: mismatched result -- got %x, want %x", test.name, + result, want) + continue + } + } +} + +// TestHashToUint256 ensures converting a hash treated as a little endian +// unsigned 256-bit value to a uint256 works as intended. +func TestHashToUint256(t *testing.T) { + t.Parallel() + + tests := []struct { + name string // test description + hash string // hash to convert + want string // expected uint256 bytes in hex + }{{ + name: "mainnet block 1 hash", + hash: "000000000000437482b6d47f82f374cde539440ddb108b0a76886f0d87d126b9", + want: "000000000000437482b6d47f82f374cde539440ddb108b0a76886f0d87d126b9", + }, { + name: "mainnet block 2 hash", + hash: "000000000000c41019872ff7db8fd2e9bfa05f42d3f8fee8e895e8c1e5b8dcba", + want: "000000000000c41019872ff7db8fd2e9bfa05f42d3f8fee8e895e8c1e5b8dcba", + }} + + for _, test := range tests { + hash, err := chainhash.NewHashFromStr(test.hash) + if err != nil { + t.Errorf("%q: unexpected err parsing test hash: %v", test.name, err) + continue + } + want := hexToUint256(test.want) + + result := HashToUint256(hash) + if !result.Eq(want) { + t.Errorf("%s: unexpected result -- got %x, want %x", test.name, + result, want) + continue + } + } +} + +// mockMainNetPowLimit returns the pow limit for the main network as of the +// time this comment was written. It is used to ensure the tests are stable +// independent of any potential changes to chain parameters. +func mockMainNetPowLimit() string { + return "00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff" +} + +// TestCheckProofOfWorkRange ensures target difficulties that are outside of +// the acceptable ranges are detected as an error and those inside are not. +func TestCheckProofOfWorkRange(t *testing.T) { + t.Parallel() + + tests := []struct { + name string // test description + bits uint32 // compact target difficulty bits to test + powLimit string // proof of work limit + err error // expected error + }{{ + name: "mainnet block 1", + bits: 0x1b01ffff, + powLimit: mockMainNetPowLimit(), + err: nil, + }, { + name: "mainnet block 288", + bits: 0x1b01330e, + powLimit: mockMainNetPowLimit(), + err: nil, + }, { + name: "smallest allowed", + bits: 0x1010000, + powLimit: mockMainNetPowLimit(), + err: nil, + }, { + name: "max allowed (exactly the pow limit)", + bits: 0x1d00ffff, + powLimit: mockMainNetPowLimit(), + err: nil, + }, { + name: "zero", + bits: 0, + powLimit: mockMainNetPowLimit(), + err: ErrUnexpectedDifficulty, + }, { + name: "negative", + bits: 0x1810000, + powLimit: mockMainNetPowLimit(), + err: ErrUnexpectedDifficulty, + }, { + name: "pow limit + 1", + bits: 0x1d010000, + powLimit: mockMainNetPowLimit(), + err: ErrUnexpectedDifficulty, + }, { + name: "max uint256 + 1 (overflows)", + bits: 0x21010000, + powLimit: mockMainNetPowLimit(), + err: ErrUnexpectedDifficulty, + }} + + for _, test := range tests { + powLimit := hexToUint256(test.powLimit) + err := CheckProofOfWorkRange(test.bits, powLimit) + if !errors.Is(err, test.err) { + t.Errorf("%q: unexpected err -- got %v, want %v", test.name, err, + test.err) + continue + } + } +} + +// TestCheckProofOfWorkRange ensures hashes and target difficulties that are +// outside of the acceptable ranges are detected as an error and those inside +// are not. +func TestCheckProofOfWork(t *testing.T) { + t.Parallel() + + tests := []struct { + name string // test description + hash string // block hash to test + bits uint32 // compact target difficulty bits to test + powLimit string // proof of work limit + err error // expected error + }{{ + name: "mainnet block 1 hash", + hash: "000000000000437482b6d47f82f374cde539440ddb108b0a76886f0d87d126b9", + bits: 0x1b01ffff, + powLimit: mockMainNetPowLimit(), + err: nil, + }, { + name: "mainnet block 288 hash", + hash: "000000000000e0ab546b8fc19f6d94054d47ffa5fe79e17611d170662c8b702b", + bits: 0x1b01330e, + powLimit: mockMainNetPowLimit(), + err: nil, + }, { + name: "max allowed (exactly the pow limit)", + hash: "0000000000001ffff00000000000000000000000000000000000000000000000", + bits: 0x1b01ffff, + powLimit: mockMainNetPowLimit(), + err: nil, + }, { + name: "high hash (pow limit + 1)", + hash: "000000000001ffff000000000000000000000000000000000000000000000001", + bits: 0x1b01ffff, + powLimit: mockMainNetPowLimit(), + err: ErrHighHash, + }, { + name: "hash satisfies target, but target too high at pow limit + 1", + hash: "0000000000000000000000000000000000000000000000000000000000000001", + bits: 0x1d010000, + powLimit: mockMainNetPowLimit(), + err: ErrUnexpectedDifficulty, + }, { + name: "zero target difficulty", + hash: "0000000000000000000000000000000000000000000000000000000000000001", + bits: 0, + powLimit: mockMainNetPowLimit(), + err: ErrUnexpectedDifficulty, + }, { + name: "negative target difficulty", + hash: "0000000000000000000000000000000000000000000000000000000000000001", + bits: 0x1810000, + powLimit: mockMainNetPowLimit(), + err: ErrUnexpectedDifficulty, + }} + + for _, test := range tests { + hash, err := chainhash.NewHashFromStr(test.hash) + if err != nil { + t.Errorf("%q: unexpected err parsing test hash: %v", test.name, err) + continue + } + powLimit := hexToUint256(test.powLimit) + + err = CheckProofOfWork(hash, test.bits, powLimit) + if !errors.Is(err, test.err) { + t.Errorf("%q: unexpected err -- got %v, want %v", test.name, err, + test.err) + continue + } + } +}