From 2c7d20442eea528e621cc2b8f9c6b60f2fa1b6a8 Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Thu, 1 Aug 2019 07:59:12 -0500 Subject: [PATCH] blockchain/standalone: Implement a new module. This implements a new module named blockchain/standalone which aims to provide several of the standalone functions currently available in blockchain and ultimately replace them in the next major version. The primary goal of offering these functions via a separate module is to reduce the required dependencies to a minimum as compared to the blockchain module. It will be ideal for applications such as lightweight clients that need to ensure basic security properties hold and calculate appropriate vote subsidies and block explorers. For example, some things an SPV wallet needs to prove are that the block headers all connect together, that they satisfy the proof of work requirements, and that a given transaction tree is valid for a given header. The new module currently only provides functions related to proof of work, however, future commits will provide functions for merkle root calculation, subsidy calculation, and coinbase transaction identification. It is being done in stages to help ease review since it is consensus critical code. Finally, it also includes comprehensive tests, full package documentation, and basic usage examples. --- blockchain/standalone/README.md | 57 ++++ blockchain/standalone/doc.go | 39 +++ blockchain/standalone/error.go | 66 +++++ blockchain/standalone/error_test.go | 103 ++++++++ blockchain/standalone/example_test.go | 78 ++++++ blockchain/standalone/go.mod | 5 + blockchain/standalone/go.sum | 4 + blockchain/standalone/pow.go | 200 ++++++++++++++ blockchain/standalone/pow_test.go | 366 ++++++++++++++++++++++++++ 9 files changed, 918 insertions(+) create mode 100644 blockchain/standalone/README.md create mode 100644 blockchain/standalone/doc.go create mode 100644 blockchain/standalone/error.go create mode 100644 blockchain/standalone/error_test.go create mode 100644 blockchain/standalone/example_test.go create mode 100644 blockchain/standalone/go.mod create mode 100644 blockchain/standalone/go.sum create mode 100644 blockchain/standalone/pow.go create mode 100644 blockchain/standalone/pow_test.go diff --git a/blockchain/standalone/README.md b/blockchain/standalone/README.md new file mode 100644 index 0000000000..85052da193 --- /dev/null +++ b/blockchain/standalone/README.md @@ -0,0 +1,57 @@ +standalone +========== + +[![Build Status](https://img.shields.io/travis/decred/dcrd.svg)](https://travis-ci.org/decred/dcrd) +[![ISC License](https://img.shields.io/badge/license-ISC-blue.svg)](http://copyfree.org) +[![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg)](https://godoc.org/github.com/decred/dcrd/blockchain/standalone) + +Package standalone provides standalone functions useful for working with the +Decred blockchain consensus rules. + +The primary goal of offering these functions via a separate module is to reduce +the required dependencies to a minimum as compared to the blockchain module. + +It is ideal for applications such as lightweight clients that need to ensure +basic security properties hold and calculate appropriate vote subsidies and +block explorers. + +For example, some things an SPV wallet needs to prove are that the block headers +all connect together, that they satisfy the proof of work requirements, and that +a given transaction tree is valid for a given header. + +The provided functions fall into the following categories: + +- Proof-of-work + - Converting to and from the compact target difficulty representation + - Calculating work values based on the compact target difficulty + - Checking a block hash satisfies a target difficulty and that target + difficulty is within a valid range +- Merkle root calculation (WIP - not yet available) +- Subsidy calculation (WIP - not yet available) +- Coinbase transaction identification (WIP - not yet available) + +## Installation and Updating + +```bash +$ go get -u github.com/decred/dcrd/blockchain/standalone +``` + +## Examples + +* [CompactToBig Example](https://godoc.org/github.com/decred/dcrd/blockchain/standalone#example-CompactToBig) + Demonstrates how to convert the compact "bits" in a block header which + represent the target difficulty to a big integer and display it using the + typical hex notation. + +* [BigToCompact Example](https://godoc.org/github.com/decred/dcrd/blockchain/standalone#example-BigToCompact) + Demonstrates how to convert a target difficulty into the compact "bits" in a + block header which represent that target difficulty. + +* [CheckProofOfWork Example](https://godoc.org/github.com/decred/dcrd/blockchain/standalone#example-CheckProofOfWork) + Demonstrates checking the proof of work of a block hash against + a target difficulty. + +## License + +Package standalone is licensed under the [copyfree](http://copyfree.org) ISC +License. diff --git a/blockchain/standalone/doc.go b/blockchain/standalone/doc.go new file mode 100644 index 0000000000..7a7ac07808 --- /dev/null +++ b/blockchain/standalone/doc.go @@ -0,0 +1,39 @@ +// Copyright (c) 2019 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +/* +Package standalone provides standalone functions useful for working with the +Decred blockchain consensus rules. + +The primary goal of offering these functions via a separate module is to reduce +the required dependencies to a minimum as compared to the blockchain module. + +It is ideal for applications such as lightweight clients that need to ensure +basic security properties hold and calculate appropriate vote subsidies and +block explorers. + +For example, some things an SPV wallet needs to prove are that the block headers +all connect together, that they satisfy the proof of work requirements, and that +a given transaction tree is valid for a given header. + +The provided functions fall into the following categories: + + - Proof-of-work + - Converting to and from the compact target difficulty representation + - Calculating work values based on the compact target difficulty + - Checking a block hash satisfies a target difficulty and that target + difficulty is within a valid range + - Merkle root calculation (WIP - not yet available) + - Subsidy calculation (WIP - not yet available) + - Coinbase transaction identification (WIP - not yet available) + +Errors + +Errors returned by this package are of type standalone.RuleError. This allows +the caller to differentiate between errors further up the call stack through +type assertions. In addition, callers can programmatically determine the +specific rule violation by examining the ErrorCode field of the type asserted +standalone.RuleError. +*/ +package standalone diff --git a/blockchain/standalone/error.go b/blockchain/standalone/error.go new file mode 100644 index 0000000000..a5f6688699 --- /dev/null +++ b/blockchain/standalone/error.go @@ -0,0 +1,66 @@ +// Copyright (c) 2019 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package standalone + +import ( + "fmt" +) + +// ErrorCode identifies a kind of error. +type ErrorCode int + +// 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 + + // ErrHighHash indicates the block does not hash to a value which is + // lower than the required target difficultly. + ErrHighHash + + // numErrorCodes is the maximum error code number used in tests. + numErrorCodes +) + +// Map of ErrorCode values back to their constant names for pretty printing. +var errorCodeStrings = map[ErrorCode]string{ + ErrUnexpectedDifficulty: "ErrUnexpectedDifficulty", + ErrHighHash: "ErrHighHash", +} + +// 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)) +} + +// 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. +type RuleError struct { + ErrorCode ErrorCode // Describes the kind of error + Description string // Human readable description of the issue +} + +// 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} +} + +// 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 +} diff --git a/blockchain/standalone/error_test.go b/blockchain/standalone/error_test.go new file mode 100644 index 0000000000..5d7527bb0e --- /dev/null +++ b/blockchain/standalone/error_test.go @@ -0,0 +1,103 @@ +// Copyright (c) 2019 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package standalone + +import ( + "testing" +) + +// TestErrorCodeStringer tests the stringized output for the ErrorCode type. +func TestErrorCodeStringer(t *testing.T) { + tests := []struct { + in ErrorCode + want string + }{ + {ErrUnexpectedDifficulty, "ErrUnexpectedDifficulty"}, + {ErrHighHash, "ErrHighHash"}, + {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() + if result != test.want { + t.Errorf("String #%d\n got: %s want: %s", i, result, test.want) + continue + } + } +} + +// TestRuleError tests the error output for the RuleError type. +func TestRuleError(t *testing.T) { + tests := []struct { + in RuleError + want string + }{{ + RuleError{Description: "duplicate block"}, + "duplicate block", + }, { + RuleError{Description: "human-readable error"}, + "human-readable error", + }, + } + + t.Logf("Running %d tests", len(tests)) + 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) + continue + } + } +} + +// TestIsErrorCode ensures IsErrorCode works as intended. +func TestIsErrorCode(t *testing.T) { + tests := []struct { + name string + err error + code ErrorCode + want bool + }{{ + name: "ErrUnexpectedDifficulty testing for ErrUnexpectedDifficulty", + err: ruleError(ErrUnexpectedDifficulty, ""), + code: ErrUnexpectedDifficulty, + want: true, + }, { + name: "ErrHighHash testing for ErrHighHash", + err: ruleError(ErrHighHash, ""), + code: ErrHighHash, + want: true, + }, { + name: "ErrHighHash error testing for ErrUnexpectedDifficulty", + err: ruleError(ErrHighHash, ""), + code: ErrUnexpectedDifficulty, + want: false, + }, { + name: "ErrHighHash error testing for unknown error code", + err: ruleError(ErrHighHash, ""), + code: 0xffff, + want: false, + }, { + name: "nil error testing for ErrUnexpectedDifficulty", + err: nil, + code: ErrUnexpectedDifficulty, + want: false, + }} + 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) + continue + } + } +} diff --git a/blockchain/standalone/example_test.go b/blockchain/standalone/example_test.go new file mode 100644 index 0000000000..ed1ff59586 --- /dev/null +++ b/blockchain/standalone/example_test.go @@ -0,0 +1,78 @@ +// Copyright (c) 2014-2016 The btcsuite developers +// Copyright (c) 2015-2019 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package standalone_test + +import ( + "fmt" + "math/big" + + "github.com/decred/dcrd/blockchain/standalone" + "github.com/decred/dcrd/chaincfg/chainhash" +) + +// This example demonstrates how to convert the compact "bits" in a block header +// which represent the target difficulty to a big integer and display it using +// the typical hex notation. +func ExampleCompactToBig() { + // Convert the bits from block 1 in the main chain. + bits := uint32(453115903) + targetDifficulty := standalone.CompactToBig(bits) + + // Display it in hex. + fmt.Printf("%064x\n", targetDifficulty.Bytes()) + + // Output: + // 000000000001ffff000000000000000000000000000000000000000000000000 +} + +// This example demonstrates how to convert a target difficulty into the compact +// "bits" in a block header which represent that target difficulty. +func ExampleBigToCompact() { + // Convert the target difficulty from block 1 in the main chain to compact + // form. + t := "000000000001ffff000000000000000000000000000000000000000000000000" + targetDifficulty, success := new(big.Int).SetString(t, 16) + if !success { + fmt.Println("invalid target difficulty") + return + } + bits := standalone.BigToCompact(targetDifficulty) + + fmt.Println(bits) + + // Output: + // 453115903 +} + +// This example demonstrates checking the proof of work of a block hash against +// a target difficulty. +func ExampleCheckProofOfWork() { + // This is the pow limit for mainnet and would ordinarily come from chaincfg + // params, however, it is hard coded here for the purposes of the example. + l := "00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + powLimit, success := new(big.Int).SetString(l, 16) + if !success { + fmt.Println("invalid pow limit") + return + } + + // Check the proof of work for block 1 in the main chain. + h := "000000000000437482b6d47f82f374cde539440ddb108b0a76886f0d87d126b9" + hash, err := chainhash.NewHashFromStr(h) + if err != nil { + fmt.Printf("failed to parse hash: %v\n", err) + return + } + bits := uint32(453115903) + + if err := standalone.CheckProofOfWork(hash, bits, powLimit); err != nil { + fmt.Printf("proof of work check failed: %v\n", err) + return + } + + // Output: + // +} diff --git a/blockchain/standalone/go.mod b/blockchain/standalone/go.mod new file mode 100644 index 0000000000..34a7f628fd --- /dev/null +++ b/blockchain/standalone/go.mod @@ -0,0 +1,5 @@ +module github.com/decred/dcrd/blockchain/standalone + +go 1.11 + +require github.com/decred/dcrd/chaincfg/chainhash v1.0.1 diff --git a/blockchain/standalone/go.sum b/blockchain/standalone/go.sum new file mode 100644 index 0000000000..ab61f49138 --- /dev/null +++ b/blockchain/standalone/go.sum @@ -0,0 +1,4 @@ +github.com/dchest/blake256 v1.0.0 h1:6gUgI5MHdz9g0TdrgKqXsoDX+Zjxmm1Sc6OsoGru50I= +github.com/dchest/blake256 v1.0.0/go.mod h1:xXNWCE1jsAP8DAjP+rKw2MbeqLczjI3TRx2VK+9OEYY= +github.com/decred/dcrd/chaincfg/chainhash v1.0.1 h1:0vG7U9+dSjSCaHQKdoSKURK2pOb47+b+8FK5q4+Je7M= +github.com/decred/dcrd/chaincfg/chainhash v1.0.1/go.mod h1:OVfvaOsNLS/A1y4Eod0Ip/Lf8qga7VXCQjUQLbkY0Go= diff --git a/blockchain/standalone/pow.go b/blockchain/standalone/pow.go new file mode 100644 index 0000000000..5e3f509818 --- /dev/null +++ b/blockchain/standalone/pow.go @@ -0,0 +1,200 @@ +// Copyright (c) 2013-2016 The btcsuite developers +// Copyright (c) 2015-2019 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package standalone + +import ( + "fmt" + "math/big" + + "github.com/decred/dcrd/chaincfg/chainhash" +) + +var ( + // bigOne is 1 represented as a big.Int. It is defined here to avoid the + // overhead of creating it multiple times. + bigOne = big.NewInt(1) + + // oneLsh256 is 1 shifted left 256 bits. It is defined here to avoid the + // overhead of creating it multiple times. + oneLsh256 = new(big.Int).Lsh(bigOne, 256) +) + +// HashToBig converts a chainhash.Hash into a big.Int that can be used to +// perform math comparisons. +func HashToBig(hash *chainhash.Hash) *big.Int { + // A Hash is in little-endian, but the big package wants the bytes in + // big-endian, so reverse them. + buf := *hash + blen := len(buf) + for i := 0; i < blen/2; i++ { + buf[i], buf[blen-1-i] = buf[blen-1-i], buf[i] + } + + return new(big.Int).SetBytes(buf[:]) +} + +// CompactToBig converts a compact representation of a whole number N to an +// unsigned 32-bit number. 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) +// +// This compact form is only used in Decred to encode unsigned 256-bit numbers +// which represent difficulty targets, thus there really is not a need for a +// sign bit, but it is implemented here to stay consistent with legacy code. +func CompactToBig(compact uint32) *big.Int { + // Extract the mantissa, sign bit, and exponent. + mantissa := compact & 0x007fffff + isNegative := compact&0x00800000 != 0 + exponent := uint(compact >> 24) + + // Since the base for the exponent is 256, the exponent can be treated as + // the number of bytes to represent the full 256-bit number. So, treat the + // exponent as the number of bytes and shift the mantissa right or left + // accordingly. This is equivalent to: + // N = mantissa * 256^(exponent-3) + var bn *big.Int + if exponent <= 3 { + mantissa >>= 8 * (3 - exponent) + bn = big.NewInt(int64(mantissa)) + } else { + bn = big.NewInt(int64(mantissa)) + bn.Lsh(bn, 8*(exponent-3)) + } + + // Make it negative if the sign bit is set. + if isNegative { + bn = bn.Neg(bn) + } + + return bn +} + +// BigToCompact converts a whole number N to a compact representation using an +// unsigned 32-bit number. 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 CompactToBig for details. +func BigToCompact(n *big.Int) uint32 { + // No need to do any work if it's zero. + if n.Sign() == 0 { + return 0 + } + + // Since the base for the exponent is 256, the exponent can be treated as + // the number of bytes. So, shift the number right or left accordingly. + // This is equivalent to: + // mantissa = mantissa / 256^(exponent-3) + var mantissa uint32 + exponent := uint(len(n.Bytes())) + if exponent <= 3 { + mantissa = uint32(n.Bits()[0]) + mantissa <<= 8 * (3 - exponent) + } else { + // Use a copy to avoid modifying the caller's original number. + tn := new(big.Int).Set(n) + mantissa = uint32(tn.Rsh(tn, 8*(exponent-3)).Bits()[0]) + } + + // 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. + compact := uint32(exponent<<24) | mantissa + if n.Sign() < 0 { + compact |= 0x00800000 + } + return compact +} + +// 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 +// CompactToBig. 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. Also, in order to avoid +// potential division by zero and really small floating point numbers, the +// result adds 1 to the denominator and multiplies the numerator by 2^256. +func CalcWork(bits uint32) *big.Int { + // Return a work value of zero if the passed difficulty bits represent a + // negative number. Note this should not happen in practice with valid + // blocks, but an invalid block could trigger it. + difficultyNum := CompactToBig(bits) + if difficultyNum.Sign() <= 0 { + return big.NewInt(0) + } + + // (1 << 256) / (difficultyNum + 1) + denominator := new(big.Int).Add(difficultyNum, bigOne) + return new(big.Int).Div(oneLsh256, denominator) +} + +// checkProofOfWorkRange ensures the provided target difficulty is in min/max +// range per the provided proof-of-work limit. +func checkProofOfWorkRange(target *big.Int, powLimit *big.Int) error { + // The target difficulty must be larger than zero. + if target.Sign() <= 0 { + str := fmt.Sprintf("target difficulty of %064x is too low", target) + return ruleError(ErrUnexpectedDifficulty, str) + } + + // The target difficulty must be less than the maximum allowed. + if target.Cmp(powLimit) > 0 { + str := fmt.Sprintf("target difficulty of %064x is higher than max of "+ + "%064x", target, powLimit) + return ruleError(ErrUnexpectedDifficulty, str) + } + + return nil +} + +// CheckProofOfWorkRange ensures the provided compact target difficulty is in +// min/max range per the provided proof-of-work limit. +func CheckProofOfWorkRange(difficultyBits uint32, powLimit *big.Int) error { + target := CompactToBig(difficultyBits) + return checkProofOfWorkRange(target, powLimit) +} + +// CheckProofOfWork ensures the provided block hash is less than the provided +// compact target difficulty and that the target difficulty is in min/max range +// per the provided proof-of-work limit. +func CheckProofOfWork(blockHash *chainhash.Hash, difficultyBits uint32, powLimit *big.Int) error { + target := CompactToBig(difficultyBits) + if err := checkProofOfWorkRange(target, powLimit); err != nil { + return err + } + + // The block hash must be less than the target difficulty. + hashNum := HashToBig(blockHash) + if hashNum.Cmp(target) > 0 { + str := fmt.Sprintf("block hash of %064x is higher than expected max "+ + "of %064x", hashNum, target) + return ruleError(ErrHighHash, str) + } + + return nil +} diff --git a/blockchain/standalone/pow_test.go b/blockchain/standalone/pow_test.go new file mode 100644 index 0000000000..cdc73de13a --- /dev/null +++ b/blockchain/standalone/pow_test.go @@ -0,0 +1,366 @@ +// Copyright (c) 2019 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package standalone + +import ( + "math/big" + "testing" + + "github.com/decred/dcrd/chaincfg/chainhash" +) + +// TestHashToBig ensures HashToBig properly converts a hash treated as a little +// endian unsigned 256-bit value to a big integer encoded with big endian. +func TestHashToBig(t *testing.T) { + tests := []struct { + name string // test description + hash string // hash to convert + want string // expected bit integer 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, success := new(big.Int).SetString(test.want, 16) + if !success { + t.Errorf("%q: unexpected err parsing test result", test.name) + continue + } + + result := HashToBig(hash) + if result.Cmp(want) != 0 { + t.Errorf("%s: unexpected result -- got %x, want %x", test.name, + result, want) + continue + } + } +} + +// TestBigToCompact ensures converting from big integers to the compact +// representation used for target difficulties produces the correct results. +func TestBigToCompact(t *testing.T) { + tests := []struct { + name string // test description + input string // big integer to test + want uint32 // expected compact value + }{{ + name: "mainnet block 1", + input: "0x000000000001ffff000000000000000000000000000000000000000000000000", + want: 0x1b01ffff, + }, { + name: "mainnet block 288", + input: "0x000000000001330e000000000000000000000000000000000000000000000000", + want: 0x1b01330e, + }, { + name: "higher diff (exponent 24, sign bit 0, mantissa 0x5fb28a)", + input: "0x00000000000000005fb28a000000000000000000000000000000000000000000", + want: 0x185fb28a, + }, { + name: "zero", + input: "0", + want: 0, + }, { + name: "-1 (exponent 1, sign bit 1, mantissa 0x10000)", + input: "-1", + want: 0x1810000, + }, { + name: "-128 (exponent 2, sign bit 1, mantissa 0x08000)", + input: "-128", + want: 0x2808000, + }, { + name: "-32768 (exponent 3, sign bit 1, mantissa 0x08000)", + input: "-32768", + want: 0x3808000, + }, { + name: "-8388608 (exponent 4, sign bit 1, mantissa 0x08000)", + input: "-8388608", + want: 0x4808000, + }} + + for _, test := range tests { + input, success := new(big.Int).SetString(test.input, 0) + if !success { + t.Errorf("%q: unexpected err parsing test input", test.name) + continue + } + + result := BigToCompact(input) + if result != test.want { + t.Errorf("%q: mismatched result -- got %x, want %x", test.name, + result, test.want) + continue + } + } +} + +// TestCompactToBig ensures converting from the compact representation used for +// target difficulties to big integers produces the correct results. +func TestCompactToBig(t *testing.T) { + tests := []struct { + name string // test description + input uint32 // compact target difficulty bits to test + want string // expected big int + }{{ + name: "mainnet block 1", + input: 0x1b01ffff, + want: "0x000000000001ffff000000000000000000000000000000000000000000000000", + }, { + name: "mainnet block 288", + input: 0x1b01330e, + want: "0x000000000001330e000000000000000000000000000000000000000000000000", + }, { + name: "higher diff (exponent 24, sign bit 0, mantissa 0x5fb28a)", + input: 0x185fb28a, + want: "0x00000000000000005fb28a000000000000000000000000000000000000000000", + }, { + name: "zero", + input: 0, + want: "0", + }, { + name: "-1 (exponent 1, sign bit 1, mantissa 0x10000)", + input: 0x1810000, + want: "-1", + }, { + name: "-128 (exponent 2, sign bit 1, mantissa 0x08000)", + input: 0x2808000, + want: "-128", + }, { + name: "-32768 (exponent 3, sign bit 1, mantissa 0x08000)", + input: 0x3808000, + want: "-32768", + }, { + name: "-8388608 (exponent 4, sign bit 1, mantissa 0x08000)", + input: 0x4808000, + want: "-8388608", + }} + + for _, test := range tests { + want, success := new(big.Int).SetString(test.want, 0) + if !success { + t.Errorf("%q: unexpected err parsing expected value", test.name) + continue + } + + result := CompactToBig(test.input) + if result.Cmp(want) != 0 { + t.Errorf("%q: mismatched result -- got %x, want %x", test.name, + result, want) + continue + } + } +} + +// TestCalcWork ensures calculating a work value from a compact target +// difficulty produces the correct results. +func TestCalcWork(t *testing.T) { + tests := []struct { + name string // test description + input uint32 // compact target difficulty bits to test + want string // expected big int + }{{ + name: "mainnet block 1", + input: 0x1b01ffff, + want: "0x0000000000000000000000000000000000000000000000000000800040002000", + }, { + name: "mainnet block 288", + input: 0x1b01330e, + want: "0x0000000000000000000000000000000000000000000000000000d56f2dcbe105", + }, { + name: "higher diff (exponent 24)", + input: 0x185fb28a, + want: "0x000000000000000000000000000000000000000000000002acd33ddd458512da", + }, { + name: "zero", + input: 0, + want: "0", + }, { + name: "negative target difficulty", + input: 0x1810000, + want: "0", + }} + + for _, test := range tests { + want, success := new(big.Int).SetString(test.want, 0) + if !success { + t.Errorf("%q: unexpected err parsing expected value", test.name) + continue + } + + result := CalcWork(test.input) + if result.Cmp(want) != 0 { + t.Errorf("%q: mismatched 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) { + 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: ruleError(ErrUnexpectedDifficulty, ""), + }, { + name: "negative", + bits: 0x1810000, + powLimit: mockMainNetPowLimit(), + err: ruleError(ErrUnexpectedDifficulty, ""), + }, { + name: "pow limit + 1", + bits: 0x1d010000, + powLimit: mockMainNetPowLimit(), + err: ruleError(ErrUnexpectedDifficulty, ""), + }} + + for _, test := range tests { + powLimit, success := new(big.Int).SetString(test.powLimit, 16) + if !success { + t.Errorf("%q: unexpected err parsing test pow limit", test.name) + continue + } + + 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 + } + 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) { + 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: ruleError(ErrHighHash, ""), + }, { + name: "hash satisfies target, but target too high at pow limit + 1", + hash: "0000000000000000000000000000000000000000000000000000000000000001", + bits: 0x1d010000, + powLimit: mockMainNetPowLimit(), + err: ruleError(ErrUnexpectedDifficulty, ""), + }, { + name: "zero target difficulty", + hash: "0000000000000000000000000000000000000000000000000000000000000001", + bits: 0, + powLimit: mockMainNetPowLimit(), + err: ruleError(ErrUnexpectedDifficulty, ""), + }, { + name: "negative target difficulty", + hash: "0000000000000000000000000000000000000000000000000000000000000001", + bits: 0x1810000, + powLimit: mockMainNetPowLimit(), + err: ruleError(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, success := new(big.Int).SetString(test.powLimit, 16) + if !success { + t.Errorf("%q: unexpected err parsing test pow limit", test.name) + continue + } + + 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 + } + continue + } + } +}