Skip to content

Commit 7ee8811

Browse files
authored
Introduce the pkg/bitcoin package (#3384)
Refs: #3349 Here we introduce the new `pkg/bitcoin` package that defines some basic Bitcoin types and interfaces required to work with the Bitcoin chain. This package is meant to reflect the part of the Bitcoin protocol to an extent required by the client applications. The `pkg/bitcoin` package is supposed to hold components specific to the Bitcoin domain and must remain free of application-specific items. Third-party helper libraries (like `btcd`) are allowed for use within this package though no external type should leak outside. Preserving those rules will allow us to achieve a clean package structure with proper dependencies. The exact types introduced by this pull request are: - `bitcoin.ByteOrder`: An enum that captures all the caveats related to the byte order used across the Bitcoin ecosystem - `bitcoin.CompactSizeUint`: A documentation type that holds the details about the CompactSize Unsigned Integer - `bitcoin.Hash`: A type that represents a 32-byte Bitcoin hash widely used as identifier in the Bitcoin protocol - `bitcoin.Transaction` & related: A type that represents a Bitcoin transaction and related types to reflect the internals - `bitcoin.BlockHeader`: A representation of the Bitcoin block header that will be used for SPV purposes - `bitcoin.Chain`: A minimal interface required to interact with the Bitcoin chain - `electrum.Client`: An example stub implementation of the `bitcoin.Chain` supposed to establish a package structure for Bitcoin interfaces implementations
2 parents 1467834 + d31a350 commit 7ee8811

7 files changed

Lines changed: 398 additions & 0 deletions

File tree

pkg/bitcoin/bitcoin.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Package bitcoin defines types and interfaces required to work with the
2+
// Bitcoin chain. This package is meant to reflect the part of the Bitcoin
3+
// protocol to an extent required by the client applications. That is,
4+
// this package can only hold the components specific to the Bitcoin domain
5+
// and must remain free of application-specific items. Third-party helper
6+
// libraries are allowed for use within this package though no external
7+
// type should leak outside.
8+
package bitcoin
9+
10+
// CompactSizeUint is a documentation type that is supposed to capture the
11+
// details of the Bitcoin's CompactSize Unsigned Integer. It represents a
12+
// number value encoded to bytes according to the following rules:
13+
//
14+
// Value | Bytes | Format
15+
// ---------------------------------------------------------------------------------------
16+
// >= 0 && <= 252 | 1 | uint8
17+
// >= 253 && <= 0xffff | 3 | 0xfd followed by the number as uint16
18+
// >= 0x10000 && <= 0xffffffff | 5 | 0xfe followed by the number as uint32
19+
// >= 0x100000000 && <= 0xffffffffffffffff | 9 | 0xff followed by the number as uint64
20+
//
21+
// For reference, see:
22+
// https://developer.bitcoin.org/reference/transactions.html#compactsize-unsigned-integers
23+
type CompactSizeUint uint64
24+
25+
// ByteOrder represents the byte order used by the Bitcoin byte arrays. The
26+
// Bitcoin ecosystem is not totally consistent in this regard and different
27+
// byte orders are used depending on the purpose.
28+
type ByteOrder int
29+
30+
const (
31+
// InternalByteOrder represents the internal byte order used by the Bitcoin
32+
// protocol. This is the primary byte order that is suitable for the
33+
// use cases related with the protocol logic and cryptography. Byte arrays
34+
// using this byte order should be converted to numbers according to
35+
// the little-endian sequence.
36+
InternalByteOrder ByteOrder = iota
37+
38+
// ReversedByteOrder represents the "human" byte order. This is the
39+
// byte order that is typically used by the third party services like
40+
// block explorers or Bitcoin chain clients. Byte arrays using this byte
41+
// order should be converted to numbers according to the big-endian
42+
// sequence. This type is also known as the `RPC Byte Order` in the
43+
// Bitcoin specification.
44+
ReversedByteOrder
45+
)

pkg/bitcoin/block.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package bitcoin
2+
3+
// BlockHeaderByteLength is the byte length of a serialized block header.
4+
const BlockHeaderByteLength = 80
5+
6+
// BlockHeader represents the header of a Bitcoin block. For reference, see:
7+
// https://developer.bitcoin.org/reference/block_chain.html#block-headers
8+
type BlockHeader struct {
9+
// Version is the block version number that indicates which set of block
10+
// validation rules to follow.
11+
Version int32
12+
// PreviousBlockHeaderHash is the hash of the previous block's header.
13+
PreviousBlockHeaderHash Hash
14+
// MerkleRootHash is a hash derived from the hashes of all transactions
15+
// included in this block.
16+
MerkleRootHash Hash
17+
// Time is a Unix epoch time when the miner started hashing the header.
18+
Time uint32
19+
// Bits determines the target threshold this block's header hash must be
20+
// less than or equal to.
21+
Bits uint32
22+
// Nonce is an arbitrary number miners change to modify the header hash
23+
// in order to produce a hash less than or equal to the target threshold.
24+
Nonce uint32
25+
}
26+
27+
// Serialize serializes the block header to a byte array using the block header
28+
// serialization format:
29+
// [Version][PreviousBlockHeaderHash][MerkleRootHash][Time][Bits][Nonce].
30+
func (bh *BlockHeader) Serialize() [BlockHeaderByteLength]byte {
31+
// TODO: Implementation of the Serialize function that concatenates all
32+
// serialized fields of the block header into a single byte array.
33+
// All numbers should be serialized to an InternalByteOrder byte array.
34+
return [BlockHeaderByteLength]byte{}
35+
}
36+
37+
// Hash calculates the block header's hash as the double SHA-256 of the
38+
// block header serialization format:
39+
// [Version][PreviousBlockHeaderHash][MerkleRootHash][Time][Bits][Nonce].
40+
func (bh *BlockHeader) Hash() Hash {
41+
// TODO: Implementation of the Hash function that consists of the following:
42+
// 1. Call bh.Serialize() to get the serialized block hash.
43+
// 2. Compute the double SHA-256 over the serialized block hash.
44+
// 3. Construct the Hash instance appropriately.
45+
return Hash{}
46+
}

pkg/bitcoin/chain.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package bitcoin
2+
3+
// Chain defines an interface meant to be used for interaction with the
4+
// Bitcoin chain.
5+
type Chain interface {
6+
// GetTransaction gets the transaction with the given transaction hash.
7+
// If the transaction with the given hash was not found on the chain,
8+
// this function returns an error.
9+
GetTransaction(transactionHash Hash) (*Transaction, error)
10+
11+
// GetTransactionConfirmations gets the number of confirmations for the
12+
// transaction with the given transaction hash. If the transaction with the
13+
// given hash was not found on the chain, this function returns an error.
14+
GetTransactionConfirmations(transactionHash Hash) (uint, error)
15+
16+
// BroadcastTransaction broadcasts the given transaction over the
17+
// network of the Bitcoin chain nodes. If the broadcast action could not be
18+
// done, this function returns an error. This function does not give any
19+
// guarantees regarding transaction mining. The transaction may be mined or
20+
// rejected eventually.
21+
BroadcastTransaction(transaction *Transaction) error
22+
23+
// GetCurrentBlockNumber gets the number of the current block. If the
24+
// current block was not determined, this function returns an error.
25+
GetCurrentBlockNumber() (uint, error)
26+
27+
// GetBlockHeader gets the block header for the given block number. If the
28+
// block with the given number was not found on the chain, this function
29+
// returns an error.
30+
GetBlockHeader(blockNumber uint) (*BlockHeader, error)
31+
}

pkg/bitcoin/electrum/electrum.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package electrum
2+
3+
import (
4+
"github.com/keep-network/keep-core/pkg/bitcoin"
5+
)
6+
7+
type Client struct{}
8+
9+
func (c *Client) GetTransaction(
10+
transactionHash bitcoin.Hash,
11+
) (*bitcoin.Transaction, error) {
12+
// TODO: Implementation.
13+
panic("not implemented")
14+
}
15+
16+
func (c *Client) GetTransactionConfirmations(
17+
transactionHash bitcoin.Hash,
18+
) (uint, error) {
19+
// TODO: Implementation.
20+
panic("not implemented")
21+
}
22+
23+
func (c *Client) BroadcastTransaction(
24+
transaction *bitcoin.Transaction,
25+
) error {
26+
// TODO: Implementation.
27+
panic("not implemented")
28+
}
29+
30+
func (c *Client) GetCurrentBlockNumber() (uint, error) {
31+
// TODO: Implementation.
32+
panic("not implemented")
33+
}
34+
35+
func (c *Client) GetBlockHeader(
36+
blockNumber uint,
37+
) (*bitcoin.BlockHeader, error) {
38+
// TODO: Implementation.
39+
panic("not implemented")
40+
}

pkg/bitcoin/hash.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package bitcoin
2+
3+
import (
4+
"encoding/hex"
5+
"fmt"
6+
)
7+
8+
// HashByteLength is the byte length of the Hash type.
9+
const HashByteLength = 32
10+
11+
// Hash represents the double SHA-256 of some arbitrary data using the
12+
// InternalByteOrder.
13+
type Hash [HashByteLength]byte
14+
15+
// NewHashFromString creates a new Hash instance using the given string.
16+
// The string is interpreted according to the given ByteOrder. That is, the
17+
// string is taken as is if the ByteOrder is InternalByteOrder and reversed if
18+
// the ByteOrder is ReversedByteOrder. The string's length must be equal
19+
// to 2*HashByteLength.
20+
func NewHashFromString(hash string, byteOrder ByteOrder) (Hash, error) {
21+
if len(hash) != 2*HashByteLength {
22+
return Hash{}, fmt.Errorf("wrong hash string size")
23+
}
24+
25+
hashBytes, err := hex.DecodeString(hash)
26+
if err != nil {
27+
return Hash{}, fmt.Errorf(
28+
"cannot decode hash string: [%w]",
29+
err,
30+
)
31+
}
32+
33+
return NewHash(hashBytes, byteOrder)
34+
}
35+
36+
// NewHash creates a new Hash instance using the given byte slice.
37+
// The byte slice is interpreted according to the given ByteOrder. That is, the
38+
// byte slice is taken as is if the ByteOrder is InternalByteOrder and reversed
39+
// if the ByteOrder is ReversedByteOrder. The byte slice's length must be equal
40+
// to HashByteLength.
41+
func NewHash(hash []byte, byteOrder ByteOrder) (Hash, error) {
42+
if len(hash) != HashByteLength {
43+
return Hash{}, fmt.Errorf("wrong hash size")
44+
}
45+
46+
var result Hash
47+
48+
switch byteOrder {
49+
case InternalByteOrder:
50+
copy(result[:], hash[:])
51+
case ReversedByteOrder:
52+
for i := 0; i < HashByteLength/2; i++ {
53+
hash[i], hash[HashByteLength-1-i] = hash[HashByteLength-1-i], hash[i]
54+
}
55+
copy(result[:], hash[:])
56+
default:
57+
panic("unknown byte order")
58+
}
59+
60+
return result, nil
61+
}
62+
63+
// String returns the unprefixed hexadecimal string representation of the Hash
64+
// in the given ByteOrder.
65+
func (h Hash) String(byteOrder ByteOrder) string {
66+
switch byteOrder {
67+
case InternalByteOrder:
68+
return hex.EncodeToString(h[:])
69+
case ReversedByteOrder:
70+
for i := 0; i < HashByteLength/2; i++ {
71+
h[i], h[HashByteLength-1-i] = h[HashByteLength-1-i], h[i]
72+
}
73+
return hex.EncodeToString(h[:])
74+
default:
75+
panic("unknown byte order")
76+
}
77+
}

pkg/bitcoin/hash_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package bitcoin
2+
3+
import (
4+
"fmt"
5+
"github.com/keep-network/keep-core/pkg/internal/testutils"
6+
"reflect"
7+
"testing"
8+
)
9+
10+
func TestHashConversions(t *testing.T) {
11+
// A hash string in the internal byte order.
12+
hashString := "5672b911ab0dcc31bb36725de6f4d0c608983da7435443d69ae47e5fc151d909"
13+
// Same hash string in the reversed byte order.
14+
reversedHashString := "09d951c15f7ee49ad6435443a73d9808c6d0f4e65d7236bb31cc0dab11b97256"
15+
16+
// Create the hash using the hash string in the internal byte order.
17+
hash, err := NewHashFromString(hashString, InternalByteOrder)
18+
if err != nil {
19+
t.Fatal(err)
20+
}
21+
22+
testutils.AssertStringsEqual(
23+
t,
24+
"hash string in the internal byte order",
25+
hashString,
26+
hash.String(InternalByteOrder),
27+
)
28+
29+
testutils.AssertStringsEqual(
30+
t,
31+
"hash string in the reversed byte order",
32+
reversedHashString,
33+
hash.String(ReversedByteOrder),
34+
)
35+
36+
// Create the same hash using the hash string in the reversed byte order.
37+
hashFromReversed, err := NewHashFromString(
38+
reversedHashString,
39+
ReversedByteOrder,
40+
)
41+
if err != nil {
42+
t.Fatal(err)
43+
}
44+
45+
// The internal representation of the hash should be the same regardless
46+
// how the hash was created.
47+
testutils.AssertBytesEqual(t, hash[:], hashFromReversed[:])
48+
49+
// Make sure we have an error if the hash string has a wrong size.
50+
_, err = NewHashFromString("0x"+hashString, InternalByteOrder)
51+
52+
expectedErr := fmt.Errorf("wrong hash string size")
53+
if !reflect.DeepEqual(expectedErr, err) {
54+
t.Errorf(
55+
"unexpected error\n"+
56+
"expected: [%v]\n"+
57+
"actual: [%v]",
58+
expectedErr,
59+
err,
60+
)
61+
}
62+
}

0 commit comments

Comments
 (0)