From 4a741df757cf6b1d1b4c0e8c875c34b650f46167 Mon Sep 17 00:00:00 2001
From: Felix Lange <fjl@twurst.com>
Date: Fri, 23 Jun 2017 10:50:49 +0200
Subject: [PATCH] common/hexutil: wrap errors in json.UnmarshalTypeError

This adds type and struct field context to error messages.
Instead of "hex string of odd length" users will now see "json: cannot
unmarshal hex string of odd length into Go struct field SendTxArgs.from
of type common.Address".
---
 common/hexutil/hexutil.go   | 25 +++++++++------
 common/hexutil/json.go      | 47 ++++++++++++++++++++-------
 common/hexutil/json_test.go | 64 ++++++++++++++++++-------------------
 common/types.go             | 15 +++++++++
 common/types_test.go        | 10 +++---
 5 files changed, 102 insertions(+), 59 deletions(-)

diff --git a/common/hexutil/hexutil.go b/common/hexutil/hexutil.go
index 6b128ae36..582a67c22 100644
--- a/common/hexutil/hexutil.go
+++ b/common/hexutil/hexutil.go
@@ -32,7 +32,6 @@ package hexutil
 
 import (
 	"encoding/hex"
-	"errors"
 	"fmt"
 	"math/big"
 	"strconv"
@@ -41,17 +40,23 @@ import (
 const uintBits = 32 << (uint64(^uint(0)) >> 63)
 
 var (
-	ErrEmptyString   = errors.New("empty hex string")
-	ErrMissingPrefix = errors.New("missing 0x prefix for hex data")
-	ErrSyntax        = errors.New("invalid hex")
-	ErrEmptyNumber   = errors.New("hex number has no digits after 0x")
-	ErrLeadingZero   = errors.New("hex number has leading zero digits after 0x")
-	ErrOddLength     = errors.New("hex string has odd length")
-	ErrUint64Range   = errors.New("hex number does not fit into 64 bits")
-	ErrUintRange     = fmt.Errorf("hex number does not fit into %d bits", uintBits)
-	ErrBig256Range   = errors.New("hex number does not fit into 256 bits")
+	ErrEmptyString   = &decError{"empty hex string"}
+	ErrSyntax        = &decError{"invalid hex string"}
+	ErrMissingPrefix = &decError{"hex string without 0x prefix"}
+	ErrOddLength     = &decError{"hex string of odd length"}
+	ErrEmptyNumber   = &decError{"hex string \"0x\""}
+	ErrLeadingZero   = &decError{"hex number with leading zero digits"}
+	ErrUint64Range   = &decError{"hex number > 64 bits"}
+	ErrUintRange     = &decError{fmt.Sprintf("hex number > %d bits", uintBits)}
+	ErrBig256Range   = &decError{"hex number > 256 bits"}
 )
 
+type decError struct{ msg string }
+
+func (err decError) Error() string {
+	return string(err.msg)
+}
+
 // Decode decodes a hex string with 0x prefix.
 func Decode(input string) ([]byte, error) {
 	if len(input) == 0 {
diff --git a/common/hexutil/json.go b/common/hexutil/json.go
index 1bc1d014c..943288fad 100644
--- a/common/hexutil/json.go
+++ b/common/hexutil/json.go
@@ -18,15 +18,19 @@ package hexutil
 
 import (
 	"encoding/hex"
-	"errors"
+	"encoding/json"
 	"fmt"
 	"math/big"
+	"reflect"
 	"strconv"
 )
 
 var (
-	textZero     = []byte(`0x0`)
-	errNonString = errors.New("cannot unmarshal non-string as hex data")
+	textZero = []byte(`0x0`)
+	bytesT   = reflect.TypeOf(Bytes(nil))
+	bigT     = reflect.TypeOf((*Big)(nil))
+	uintT    = reflect.TypeOf(Uint(0))
+	uint64T  = reflect.TypeOf(Uint64(0))
 )
 
 // Bytes marshals/unmarshals as a JSON string with 0x prefix.
@@ -44,9 +48,9 @@ func (b Bytes) MarshalText() ([]byte, error) {
 // UnmarshalJSON implements json.Unmarshaler.
 func (b *Bytes) UnmarshalJSON(input []byte) error {
 	if !isString(input) {
-		return errNonString
+		return errNonString(bytesT)
 	}
-	return b.UnmarshalText(input[1 : len(input)-1])
+	return wrapTypeError(b.UnmarshalText(input[1:len(input)-1]), bytesT)
 }
 
 // UnmarshalText implements encoding.TextUnmarshaler.
@@ -69,6 +73,16 @@ func (b Bytes) String() string {
 	return Encode(b)
 }
 
+// UnmarshalFixedJSON decodes the input as a string with 0x prefix. The length of out
+// determines the required input length. This function is commonly used to implement the
+// UnmarshalJSON method for fixed-size types.
+func UnmarshalFixedJSON(typ reflect.Type, input, out []byte) error {
+	if !isString(input) {
+		return errNonString(typ)
+	}
+	return wrapTypeError(UnmarshalFixedText(typ.String(), input[1:len(input)-1], out), typ)
+}
+
 // UnmarshalFixedText decodes the input as a string with 0x prefix. The length of out
 // determines the required input length. This function is commonly used to implement the
 // UnmarshalText method for fixed-size types.
@@ -127,9 +141,9 @@ func (b Big) MarshalText() ([]byte, error) {
 // UnmarshalJSON implements json.Unmarshaler.
 func (b *Big) UnmarshalJSON(input []byte) error {
 	if !isString(input) {
-		return errNonString
+		return errNonString(bigT)
 	}
-	return b.UnmarshalText(input[1 : len(input)-1])
+	return wrapTypeError(b.UnmarshalText(input[1:len(input)-1]), bigT)
 }
 
 // UnmarshalText implements encoding.TextUnmarshaler
@@ -189,9 +203,9 @@ func (b Uint64) MarshalText() ([]byte, error) {
 // UnmarshalJSON implements json.Unmarshaler.
 func (b *Uint64) UnmarshalJSON(input []byte) error {
 	if !isString(input) {
-		return errNonString
+		return errNonString(uint64T)
 	}
-	return b.UnmarshalText(input[1 : len(input)-1])
+	return wrapTypeError(b.UnmarshalText(input[1:len(input)-1]), uint64T)
 }
 
 // UnmarshalText implements encoding.TextUnmarshaler
@@ -233,9 +247,9 @@ func (b Uint) MarshalText() ([]byte, error) {
 // UnmarshalJSON implements json.Unmarshaler.
 func (b *Uint) UnmarshalJSON(input []byte) error {
 	if !isString(input) {
-		return errNonString
+		return errNonString(uintT)
 	}
-	return b.UnmarshalText(input[1 : len(input)-1])
+	return wrapTypeError(b.UnmarshalText(input[1:len(input)-1]), uintT)
 }
 
 // UnmarshalText implements encoding.TextUnmarshaler.
@@ -295,3 +309,14 @@ func checkNumberText(input []byte) (raw []byte, err error) {
 	}
 	return input, nil
 }
+
+func wrapTypeError(err error, typ reflect.Type) error {
+	if _, ok := err.(*decError); ok {
+		return &json.UnmarshalTypeError{Value: err.Error(), Type: typ}
+	}
+	return err
+}
+
+func errNonString(typ reflect.Type) error {
+	return &json.UnmarshalTypeError{Value: "non-string", Type: typ}
+}
diff --git a/common/hexutil/json_test.go b/common/hexutil/json_test.go
index e4e827491..8a6b8643a 100644
--- a/common/hexutil/json_test.go
+++ b/common/hexutil/json_test.go
@@ -62,12 +62,12 @@ var errJSONEOF = errors.New("unexpected end of JSON input")
 var unmarshalBytesTests = []unmarshalTest{
 	// invalid encoding
 	{input: "", wantErr: errJSONEOF},
-	{input: "null", wantErr: errNonString},
-	{input: "10", wantErr: errNonString},
-	{input: `"0"`, wantErr: ErrMissingPrefix},
-	{input: `"0x0"`, wantErr: ErrOddLength},
-	{input: `"0xxx"`, wantErr: ErrSyntax},
-	{input: `"0x01zz01"`, wantErr: ErrSyntax},
+	{input: "null", wantErr: errNonString(bytesT)},
+	{input: "10", wantErr: errNonString(bytesT)},
+	{input: `"0"`, wantErr: wrapTypeError(ErrMissingPrefix, bytesT)},
+	{input: `"0x0"`, wantErr: wrapTypeError(ErrOddLength, bytesT)},
+	{input: `"0xxx"`, wantErr: wrapTypeError(ErrSyntax, bytesT)},
+	{input: `"0x01zz01"`, wantErr: wrapTypeError(ErrSyntax, bytesT)},
 
 	// valid encoding
 	{input: `""`, want: referenceBytes("")},
@@ -127,16 +127,16 @@ func TestMarshalBytes(t *testing.T) {
 var unmarshalBigTests = []unmarshalTest{
 	// invalid encoding
 	{input: "", wantErr: errJSONEOF},
-	{input: "null", wantErr: errNonString},
-	{input: "10", wantErr: errNonString},
-	{input: `"0"`, wantErr: ErrMissingPrefix},
-	{input: `"0x"`, wantErr: ErrEmptyNumber},
-	{input: `"0x01"`, wantErr: ErrLeadingZero},
-	{input: `"0xx"`, wantErr: ErrSyntax},
-	{input: `"0x1zz01"`, wantErr: ErrSyntax},
+	{input: "null", wantErr: errNonString(bigT)},
+	{input: "10", wantErr: errNonString(bigT)},
+	{input: `"0"`, wantErr: wrapTypeError(ErrMissingPrefix, bigT)},
+	{input: `"0x"`, wantErr: wrapTypeError(ErrEmptyNumber, bigT)},
+	{input: `"0x01"`, wantErr: wrapTypeError(ErrLeadingZero, bigT)},
+	{input: `"0xx"`, wantErr: wrapTypeError(ErrSyntax, bigT)},
+	{input: `"0x1zz01"`, wantErr: wrapTypeError(ErrSyntax, bigT)},
 	{
 		input:   `"0x10000000000000000000000000000000000000000000000000000000000000000"`,
-		wantErr: ErrBig256Range,
+		wantErr: wrapTypeError(ErrBig256Range, bigT),
 	},
 
 	// valid encoding
@@ -208,14 +208,14 @@ func TestMarshalBig(t *testing.T) {
 var unmarshalUint64Tests = []unmarshalTest{
 	// invalid encoding
 	{input: "", wantErr: errJSONEOF},
-	{input: "null", wantErr: errNonString},
-	{input: "10", wantErr: errNonString},
-	{input: `"0"`, wantErr: ErrMissingPrefix},
-	{input: `"0x"`, wantErr: ErrEmptyNumber},
-	{input: `"0x01"`, wantErr: ErrLeadingZero},
-	{input: `"0xfffffffffffffffff"`, wantErr: ErrUint64Range},
-	{input: `"0xx"`, wantErr: ErrSyntax},
-	{input: `"0x1zz01"`, wantErr: ErrSyntax},
+	{input: "null", wantErr: errNonString(uint64T)},
+	{input: "10", wantErr: errNonString(uint64T)},
+	{input: `"0"`, wantErr: wrapTypeError(ErrMissingPrefix, uint64T)},
+	{input: `"0x"`, wantErr: wrapTypeError(ErrEmptyNumber, uint64T)},
+	{input: `"0x01"`, wantErr: wrapTypeError(ErrLeadingZero, uint64T)},
+	{input: `"0xfffffffffffffffff"`, wantErr: wrapTypeError(ErrUint64Range, uint64T)},
+	{input: `"0xx"`, wantErr: wrapTypeError(ErrSyntax, uint64T)},
+	{input: `"0x1zz01"`, wantErr: wrapTypeError(ErrSyntax, uint64T)},
 
 	// valid encoding
 	{input: `""`, want: uint64(0)},
@@ -298,15 +298,15 @@ var (
 var unmarshalUintTests = []unmarshalTest{
 	// invalid encoding
 	{input: "", wantErr: errJSONEOF},
-	{input: "null", wantErr: errNonString},
-	{input: "10", wantErr: errNonString},
-	{input: `"0"`, wantErr: ErrMissingPrefix},
-	{input: `"0x"`, wantErr: ErrEmptyNumber},
-	{input: `"0x01"`, wantErr: ErrLeadingZero},
-	{input: `"0x100000000"`, want: uint(maxUint33bits), wantErr32bit: ErrUintRange},
-	{input: `"0xfffffffffffffffff"`, wantErr: ErrUintRange},
-	{input: `"0xx"`, wantErr: ErrSyntax},
-	{input: `"0x1zz01"`, wantErr: ErrSyntax},
+	{input: "null", wantErr: errNonString(uintT)},
+	{input: "10", wantErr: errNonString(uintT)},
+	{input: `"0"`, wantErr: wrapTypeError(ErrMissingPrefix, uintT)},
+	{input: `"0x"`, wantErr: wrapTypeError(ErrEmptyNumber, uintT)},
+	{input: `"0x01"`, wantErr: wrapTypeError(ErrLeadingZero, uintT)},
+	{input: `"0x100000000"`, want: uint(maxUint33bits), wantErr32bit: wrapTypeError(ErrUintRange, uintT)},
+	{input: `"0xfffffffffffffffff"`, wantErr: wrapTypeError(ErrUintRange, uintT)},
+	{input: `"0xx"`, wantErr: wrapTypeError(ErrSyntax, uintT)},
+	{input: `"0x1zz01"`, wantErr: wrapTypeError(ErrSyntax, uintT)},
 
 	// valid encoding
 	{input: `""`, want: uint(0)},
@@ -317,7 +317,7 @@ var unmarshalUintTests = []unmarshalTest{
 	{input: `"0x1122aaff"`, want: uint(0x1122aaff)},
 	{input: `"0xbbb"`, want: uint(0xbbb)},
 	{input: `"0xffffffff"`, want: uint(0xffffffff)},
-	{input: `"0xffffffffffffffff"`, want: uint(maxUint64bits), wantErr32bit: ErrUintRange},
+	{input: `"0xffffffffffffffff"`, want: uint(maxUint64bits), wantErr32bit: wrapTypeError(ErrUintRange, uintT)},
 }
 
 func TestUnmarshalUint(t *testing.T) {
diff --git a/common/types.go b/common/types.go
index 05288bf46..803726634 100644
--- a/common/types.go
+++ b/common/types.go
@@ -31,6 +31,11 @@ const (
 	AddressLength = 20
 )
 
+var (
+	hashT    = reflect.TypeOf(Hash{})
+	addressT = reflect.TypeOf(Address{})
+)
+
 // Hash represents the 32 byte Keccak256 hash of arbitrary data.
 type Hash [HashLength]byte
 
@@ -72,6 +77,11 @@ func (h *Hash) UnmarshalText(input []byte) error {
 	return hexutil.UnmarshalFixedText("Hash", input, h[:])
 }
 
+// UnmarshalJSON parses a hash in hex syntax.
+func (h *Hash) UnmarshalJSON(input []byte) error {
+	return hexutil.UnmarshalFixedJSON(hashT, input, h[:])
+}
+
 // MarshalText returns the hex representation of h.
 func (h Hash) MarshalText() ([]byte, error) {
 	return hexutil.Bytes(h[:]).MarshalText()
@@ -194,6 +204,11 @@ func (a *Address) UnmarshalText(input []byte) error {
 	return hexutil.UnmarshalFixedText("Address", input, a[:])
 }
 
+// UnmarshalJSON parses a hash in hex syntax.
+func (a *Address) UnmarshalJSON(input []byte) error {
+	return hexutil.UnmarshalFixedJSON(addressT, input, a[:])
+}
+
 // UnprefixedHash allows marshaling an Address without 0x prefix.
 type UnprefixedAddress Address
 
diff --git a/common/types_test.go b/common/types_test.go
index 9f9d8b767..154c33063 100644
--- a/common/types_test.go
+++ b/common/types_test.go
@@ -21,8 +21,6 @@ import (
 	"math/big"
 	"strings"
 	"testing"
-
-	"github.com/ethereum/go-ethereum/common/hexutil"
 )
 
 func TestBytesConversion(t *testing.T) {
@@ -43,10 +41,10 @@ func TestHashJsonValidation(t *testing.T) {
 		Size   int
 		Error  string
 	}{
-		{"", 62, hexutil.ErrMissingPrefix.Error()},
-		{"0x", 66, "hex string has length 66, want 64 for Hash"},
-		{"0x", 63, hexutil.ErrOddLength.Error()},
-		{"0x", 0, "hex string has length 0, want 64 for Hash"},
+		{"", 62, "json: cannot unmarshal hex string without 0x prefix into Go value of type common.Hash"},
+		{"0x", 66, "hex string has length 66, want 64 for common.Hash"},
+		{"0x", 63, "json: cannot unmarshal hex string of odd length into Go value of type common.Hash"},
+		{"0x", 0, "hex string has length 0, want 64 for common.Hash"},
 		{"0x", 64, ""},
 		{"0X", 64, ""},
 	}
-- 
GitLab