Repository: summa-tx/bitcoin-spv (language: rust)

Issue:
Add utils_test.go



Discussion / hints:

Fix patch:
diff --git a/golang/btcspv/types.go b/golang/btcspv/types.go
index 4e7b09b4..48708006 100644
--- a/golang/btcspv/types.go
+++ b/golang/btcspv/types.go
@@ -38,15 +38,15 @@ func (h *HexBytes) UnmarshalJSON(b []byte) error {
 	if err != nil {
 		return err
 	}
-	copy(*h, buf)
 
+	*h = append(*h, buf...)
 	return nil
 }
 
-// MarshallJSON marashalls bytestrings as 0x-prepended hex
-func (h HexBytes) MarshallJSON() ([]byte, error) {
-	encoded := hex.EncodeToString(h)
-	return []byte("0x" + encoded), nil
+// MarshalJSON marashalls bytestrings as 0x-prepended hex
+func (h HexBytes) MarshalJSON() ([]byte, error) {
+	encoded := "\"0x" + hex.EncodeToString(h[:]) + "\""
+	return []byte(encoded), nil
 }
 
 // UnmarshalJSON unmarshalls 32 byte digests
@@ -65,8 +65,8 @@ func (h *Hash256Digest) UnmarshalJSON(b []byte) error {
 	return nil
 }
 
-// MarshallJSON marashalls 32 byte digests as 0x-prepended hex
-func (h Hash256Digest) MarshallJSON() ([]byte, error) {
-	encoded := hex.EncodeToString(h[:])
-	return []byte("0x" + encoded), nil
+// MarshalJSON marashalls 32 byte digests as 0x-prepended hex
+func (h Hash256Digest) MarshalJSON() ([]byte, error) {
+	encoded := "\"0x" + hex.EncodeToString(h[:]) + "\""
+	return []byte(encoded), nil
 }
diff --git a/golang/btcspv/utils.go b/golang/btcspv/utils.go
index ac828c6b..4d293968 100644
--- a/golang/btcspv/utils.go
+++ b/golang/btcspv/utils.go
@@ -1,12 +1,18 @@
 package btcspv
 
 import (
+	"bytes"
 	"encoding/hex"
+	"errors"
+	"fmt"
 
 	"github.com/btcsuite/btcutil/base58"
 	"github.com/btcsuite/btcutil/bech32"
 )
 
+const zeroBytesError = "Attempting to encode empty bytestring. " +
+	"Hint: your payload may not be properly initialized"
+
 func strip0xPrefix(s string) string {
 	if len(s) < 2 {
 		return s
@@ -65,32 +71,59 @@ func GetInputType(inputType InputType) string {
 }
 
 // EncodeP2SH turns a scripthash into an address
-func EncodeP2SH(sh []byte) string {
-	return base58.CheckEncode(sh, 5)
+func EncodeP2SH(sh []byte) (string, error) {
+	if len(sh) != 20 {
+		return "", fmt.Errorf("SH must be 20 bytes, got %d bytes", len(sh))
+	}
+	if bytes.Equal(sh, make([]byte, len(sh))) {
+		return "", errors.New(zeroBytesError)
+	}
+	return base58.CheckEncode(sh, 5), nil
 }
 
 // EncodeP2PKH turns a pubkey hash into an address
-func EncodeP2PKH(pkh []byte) string {
-	return base58.CheckEncode(pkh, 0)
+func EncodeP2PKH(pkh []byte) (string, error) {
+	if len(pkh) != 20 {
+		return "", fmt.Errorf("PKH must be 20 bytes, got %d bytes", len(pkh))
+	}
+	if bytes.Equal(pkh, make([]byte, len(pkh))) {
+		return "", errors.New(zeroBytesError)
+
+	}
+	return base58.CheckEncode(pkh, 0), nil
 }
 
-func encodeSegWit(payload []byte, version int) string {
-	adj, err := bech32.ConvertBits(payload, 8, 5, true)
-	if err != nil {
-		return ""
+func encodeSegWit(payload []byte, version int) (string, error) {
+	if bytes.Equal(payload, make([]byte, len(payload))) {
+		return "", errors.New(zeroBytesError)
 	}
+	adj, _ := bech32.ConvertBits(payload, 8, 5, true)
 	combined := []byte{0x00}
 	combined = append(combined, adj...)
 	res, _ := bech32.Encode("bc", combined)
-	return res
+	return res, nil
 }
 
 // EncodeP2WSH turns a scripthash into an address
-func EncodeP2WSH(sh []byte) string {
-	return encodeSegWit(sh, 0)
+func EncodeP2WSH(sh []byte) (string, error) {
+	if len(sh) != 32 {
+		return "", fmt.Errorf("WSH must be 32 bytes, got %d bytes", len(sh))
+	}
+	addr, err := encodeSegWit(sh, 0)
+	if err != nil {
+		return "", err
+	}
+	return addr, nil
 }
 
 // EncodeP2WPKH turns a pubkey hash into an address
-func EncodeP2WPKH(pkh []byte) string {
-	return encodeSegWit(pkh, 0)
+func EncodeP2WPKH(pkh []byte) (string, error) {
+	if len(pkh) != 20 {
+		return "", fmt.Errorf("WPKH must be 20 bytes, got %d bytes", len(pkh))
+	}
+	addr, err := encodeSegWit(pkh, 0)
+	if err != nil {
+		return "", err
+	}
+	return addr, nil
 }


Test patch:
diff --git a/golang/btcspv/bitcoin_spv_test.go b/golang/btcspv/bitcoin_spv_test.go
index 8dc0718b..fe774c60 100644
--- a/golang/btcspv/bitcoin_spv_test.go
+++ b/golang/btcspv/bitcoin_spv_test.go
@@ -32,7 +32,11 @@ func (t *TestCase) UnmarshalJSON(b []byte) error {
 
 	switch data["input"].(type) {
 	case string:
-		t.Input = DecodeIfHex(data["input"].(string))
+		if len(data["input"].(string)) >= 2 && data["input"].(string)[0:2] == "0x" {
+			t.Input = DecodeIfHex(data["input"].(string))
+		} else {
+			t.Input = data["input"].(string)
+		}
 	case float64:
 		t.Input = int(data["input"].(float64))
 	default:
@@ -41,7 +45,11 @@ func (t *TestCase) UnmarshalJSON(b []byte) error {
 
 	switch data["output"].(type) {
 	case string:
-		t.Output = DecodeIfHex(data["output"].(string))
+		if len(data["output"].(string)) >= 2 && data["output"].(string)[0:2] == "0x" {
+			t.Output = DecodeIfHex(data["output"].(string))
+		} else {
+			t.Output = data["output"].(string)
+		}
 	case float64:
 		t.Output = int(data["output"].(float64))
 	default:
diff --git a/golang/btcspv/types_test.go b/golang/btcspv/types_test.go
index 2b9596e6..b6875e80 100644
--- a/golang/btcspv/types_test.go
+++ b/golang/btcspv/types_test.go
@@ -1,6 +1,7 @@
 package btcspv
 
 import (
+	"encoding/hex"
 	"encoding/json"
 	"io/ioutil"
 	"os"
@@ -38,7 +39,7 @@ func TestTypes(t *testing.T) {
 	suite.Run(t, typesSuite)
 }
 
-func (suite *TypesSuite) TestUnmarshallSPVProof() {
+func (suite *TypesSuite) TestUnmarshalSPVProof() {
 	valid := suite.Fixtures.Valid
 
 	for i := range valid {
@@ -49,21 +50,42 @@ func (suite *TypesSuite) TestUnmarshallSPVProof() {
 	}
 }
 
-func (suite *TypesSuite) TestUnmarshallBadHexBytes() {
+func (suite *TypesSuite) TestMarshalSPVProof() {
+	valid := suite.Fixtures.Valid
+	spvProof := new(SPVProof)
+	json.Unmarshal([]byte(valid[0]), &spvProof)
+
+	// Extra assertions here will catch random broken stuff
+	suite.Equal(
+		"74d6d6dc1fc9b0f393abde12e76adeeb3d674b38b7fbea4d9fc28b3bb0f67651",
+		hex.EncodeToString(spvProof.TxID[:]))
+	suite.Equal(
+		"5176f6b03b8bc29f4deafbb7384b673debde6ae712deab93f3b0c91fdcd6d674",
+		hex.EncodeToString(spvProof.TxIDLE[:]))
+	suite.Equal(uint32(26), spvProof.Index)
+	// // TODO: assert header equalities
+	// fmt.Println(spvProof.ConfirmingHeader)
+	suite.Equal(384, len(spvProof.IntermediateNodes))
+
+	_, err := json.Marshal(spvProof)
+	suite.Nil(err)
+}
+
+func (suite *TypesSuite) TestUnmarshalBadHexBytes() {
 	badHexBytes := suite.Fixtures.BadHexBytes
 	s := new(SPVProof)
 	err := json.Unmarshal([]byte(badHexBytes), &s)
 	suite.EqualError(err, "encoding/hex: invalid byte: U+0051 'Q'")
 }
 
-func (suite *TypesSuite) TestUnmarshallBadHexHash256() {
+func (suite *TypesSuite) TestUnmarshalBadHexHash256() {
 	badHexHash256 := suite.Fixtures.BadHexHash256
 	s := new(SPVProof)
 	err := json.Unmarshal([]byte(badHexHash256), &s)
 	suite.EqualError(err, "encoding/hex: invalid byte: U+0051 'Q'")
 }
 
-func (suite *TypesSuite) TestUnmarshallBadLenHash256() {
+func (suite *TypesSuite) TestUnmarshalBadLenHash256() {
 	badLenHash256 := suite.Fixtures.BadLenHash256
 	s := new(SPVProof)
 	err := json.Unmarshal([]byte(badLenHash256), &s)
diff --git a/golang/btcspv/utils_test.go b/golang/btcspv/utils_test.go
new file mode 100644
index 00000000..9e9e0ed1
--- /dev/null
+++ b/golang/btcspv/utils_test.go
@@ -0,0 +1,180 @@
+package btcspv
+
+func (suite *UtilsSuite) TestStrip0xPrefix() {
+	suite.Equal("0", strip0xPrefix("0"))
+	suite.Equal("", strip0xPrefix(""))
+	suite.Equal("333", strip0xPrefix("0x333"))
+}
+
+func (suite *UtilsSuite) TestDecodeIfHex() {
+	var expected []byte
+	var actual []byte
+
+	expected = []byte{0}
+	actual = DecodeIfHex("0x00")
+	suite.Equal(expected, actual)
+
+	expected = []byte{0}
+	actual = DecodeIfHex("00")
+	suite.Equal(expected, actual)
+
+	expected = []byte{0, 1, 2, 42, 100, 101, 102, 255}
+	actual = DecodeIfHex("0x0001022a646566ff")
+	suite.Equal(expected, actual)
+
+	suite.Equal([]byte{0xab, 0xcd}, DecodeIfHex("abcd"))
+	suite.Equal([]byte("qqqq"), DecodeIfHex("qqqq"))
+	suite.Equal([]byte("foo"), DecodeIfHex("foo"))
+	suite.Equal([]byte("d"), DecodeIfHex("d"))
+	suite.Equal([]byte(""), DecodeIfHex(""))
+
+}
+
+func (suite *UtilsSuite) TestGetOutputType() {
+	var expected string
+	var actual string
+
+	expected = "Output None"
+	actual = GetOutputType(0)
+	suite.Equal(expected, actual)
+
+	expected = "WPKH"
+	actual = GetOutputType(1)
+	suite.Equal(expected, actual)
+
+	expected = "WSH"
+	actual = GetOutputType(2)
+	suite.Equal(expected, actual)
+
+	expected = "Op Return"
+	actual = GetOutputType(3)
+	suite.Equal(expected, actual)
+
+	expected = "PKH"
+	actual = GetOutputType(4)
+	suite.Equal(expected, actual)
+
+	expected = "SH"
+	actual = GetOutputType(5)
+	suite.Equal(expected, actual)
+
+	expected = "Nonstandard"
+	actual = GetOutputType(6)
+	suite.Equal(expected, actual)
+}
+
+func (suite *UtilsSuite) TestGetInputType() {
+	var expected string
+	var actual string
+
+	expected = "Input None"
+	actual = GetInputType(0)
+	suite.Equal(expected, actual)
+
+	expected = "Legacy"
+	actual = GetInputType(1)
+	suite.Equal(expected, actual)
+
+	expected = "Compatibility"
+	actual = GetInputType(2)
+	suite.Equal(expected, actual)
+
+	expected = "Witness"
+	actual = GetInputType(3)
+	suite.Equal(expected, actual)
+}
+
+func (suite *UtilsSuite) TestEncodeP2SH() {
+	fixture := suite.Fixtures["encodeP2SH"]
+
+	for i := range fixture {
+		testCase := fixture[i]
+
+		input := testCase.Input.([]byte)
+		expected := testCase.Output
+		actual, err := EncodeP2SH(input)
+		suite.Nil(err)
+		suite.Equal(expected, actual)
+	}
+}
+
+func (suite *UtilsSuite) TestEncodeP2PKH() {
+	fixture := suite.Fixtures["encodeP2PKH"]
+
+	for i := range fixture {
+		testCase := fixture[i]
+
+		input := testCase.Input.([]byte)
+		expected := testCase.Output
+		actual, err := EncodeP2PKH(input)
+		suite.Nil(err)
+		suite.Equal(expected, actual)
+	}
+}
+
+func (suite *UtilsSuite) TestEncodeP2WSH() {
+	fixture := suite.Fixtures["encodeP2WSH"]
+
+	for i := range fixture {
+		testCase := fixture[i]
+
+		input := testCase.Input.([]byte)
+		expected := testCase.Output
+		actual, err := EncodeP2WSH(input)
+		suite.Nil(err)
+		suite.Equal(expected, actual)
+	}
+}
+
+func (suite *UtilsSuite) TestEncodeP2WPKH() {
+	fixture := suite.Fixtures["encodeP2WPKH"]
+
+	for i := range fixture {
+		testCase := fixture[i]
+
+		input := testCase.Input.([]byte)
+		expected := testCase.Output
+		actual, err := EncodeP2WPKH(input)
+		suite.Nil(err)
+		suite.Equal(expected, actual)
+	}
+}
+
+func (suite *UtilsSuite) TestEncodeSegwitErrors() {
+	// All 0s
+	input := make([]byte, 20)
+	actual, err := EncodeP2PKH(input)
+	suite.Equal("", actual)
+	suite.EqualError(err, zeroBytesError)
+
+	actual, err = EncodeP2SH(input)
+	suite.Equal("", actual)
+	suite.EqualError(err, zeroBytesError)
+
+	actual, err = EncodeP2WPKH(input)
+	suite.Equal("", actual)
+	suite.EqualError(err, zeroBytesError)
+
+	input = make([]byte, 32)
+	actual, err = EncodeP2WSH(input)
+	suite.Equal("", actual)
+	suite.EqualError(err, zeroBytesError)
+
+	// Wrong Length
+	input = make([]byte, 1)
+	actual, err = EncodeP2PKH(input)
+	suite.Equal("", actual)
+	suite.EqualError(err, "PKH must be 20 bytes, got 1 bytes")
+
+	actual, err = EncodeP2SH(input)
+	suite.Equal("", actual)
+	suite.EqualError(err, "SH must be 20 bytes, got 1 bytes")
+
+	actual, err = EncodeP2WPKH(input)
+	suite.Equal("", actual)
+	suite.EqualError(err, "WPKH must be 20 bytes, got 1 bytes")
+
+	actual, err = EncodeP2WSH(input)
+	suite.Equal("", actual)
+	suite.EqualError(err, "WSH must be 32 bytes, got 1 bytes")
+}
diff --git a/testVectors.json b/testVectors.json
index cedc273c..44013df8 100644
--- a/testVectors.json
+++ b/testVectors.json
@@ -1277,5 +1277,73 @@
       ],
       "output": 6393023717201
     }
+  ],
+  "encodeP2SH": [
+    {
+      "input": "0x15bcb3247bad4498fb5b5a88f65d2b3ceec9ebf4",
+      "output": "33fxAfNiDgjQhmZgVF1nZuxjSZtpJReSUa"
+    },
+    {
+      "input": "0x24f0a01c93ef0e9f95bf4af2b38ffec4beb5943d",
+      "output": "354LWQ3HtMNPDkA6W91biR8PqAxr2y7bat"
+    },
+    {
+      "input": "0x2d8b26c905309fc24a367d270255fd3f7cb58e88",
+      "output": "35qq2bJLSZnZt8Z9SweBEEfCmUf4DEKooh"
+    },
+    {
+      "input": "0x2da2c8ff01b5a735fde593762307b1ab287adc59",
+      "output": "35rKLgjzugHA2xdUh9gvP2rvDMDRsoKHgB"
+    }
+  ],
+  "encodeP2PKH": [
+    {
+      "input": "0x0dce68619d07abbb38f628744d114063148c505e",
+      "output": "12G13xPUNi2i3hBpS6emYRR9z5g75W89Dd"
+    },
+    {
+      "input": "0x12f95d63041bec7471573c898472950747a19863",
+      "output": "12jKtd6vq16ftSo4WCeV2yEGS6g4qM5i2z"
+    },
+    {
+      "input": "0x0f951bc38ec25d2ba1075ff051e9cc36f4649c81",
+      "output": "12RPm7dmeSBcHhP2t9Qfx5ZJS4K388CJFh"
+    },
+    {
+      "input": "0x1ac86542ab8909722f6e71f576a9bfbc09719842",
+      "output": "13ScdiSFkiAvn7Ay73SBkF8HjPRChvPvKg"
+    }
+  ],
+  "encodeP2WSH": [
+    {
+      "input": "0x701a8d401c84fb13e6baf169d59684e17abd9fa216c8cc5b9fc63d622ff8c58d",
+      "output": "bc1qwqdg6squsna38e46795at95yu9atm8azzmyvckulcc7kytlcckxswvvzej"
+    },
+    {
+      "input": "0xf6b2f54f2aa08ac04c74aaca151fdcd754446afe4d3ce2e7f8913b2de92cd141",
+      "output": "bc1q76e02ne25z9vqnr54t9p287u6a2yg6h7f57w9elcjyajm6fv69qsx27mv2"
+    },
+    {
+      "input": "0x8301e8dba2ed6990c738072b8992f5e61664022679f4d9ebdee5214ef58d9ea3",
+      "output": "bc1qsvq73kaza45ep3ecqu4cnyh4uctxgq3x086dn677u5s5aavdn63svuf2u9"
+    }
+  ],
+  "encodeP2WPKH": [
+    {
+      "input": "0x88821c6bfeb815b43feb6d523d414843e586d6a6",
+      "output": "bc1q3zppc6l7hq2mg0ltd4fr6s2gg0jcd44xmqf2uq"
+    },
+    {
+      "input": "0x9d0536f746d77e3f337c86a2e588fc586097631f",
+      "output": "bc1qn5znda6x6alr7vmus63wtz8utpsfwcclvtn6cq"
+    },
+    {
+      "input": "0x83c365d654cfb273770dd1e25147c2c4e9c17f26",
+      "output": "bc1qs0pkt4j5e7e8xacd6839z37zcn5uzlexxwwwd8"
+    },
+    {
+      "input": "0x231ff20ccbdaa0ee2bff6dd71ca2caf12bc2ec93",
+      "output": "bc1qyv0lyrxtm2swu2lldht3egk27y4u9mynrzxm7c"
+    }
   ]
 }