rpc: simplify the encoding of interface-typed arguments in JSON (#7600)

Add package jsontypes that implements a subset of the custom libs/json 
package. Specifically it handles encoding and decoding of interface types
wrapped in "tagged" JSON objects. It omits the deep reflection on arbitrary
types, preserving only the handling of type tags wrapper encoding.

- Register interface types (Evidence, PubKey, PrivKey) for tagged encoding.
- Update the existing implementations to satisfy the type.
- Register those types with the jsontypes registry.
- Add string tags to 64-bit integer fields where needed.
- Add marshalers to structs that export interface-typed fields.
This commit is contained in:
M. J. Fromberger
2022-01-14 18:14:09 -08:00
committed by GitHub
parent 7ed57ef5f9
commit dbe2146d0a
19 changed files with 349 additions and 24 deletions
+109
View File
@@ -0,0 +1,109 @@
// Package jsontypes supports decoding for interface types whose concrete
// implementations need to be stored as JSON. To do this, concrete values are
// packaged in wrapper objects having the form:
//
// {
// "type": "<type-tag>",
// "value": <json-encoding-of-value>
// }
//
// This package provides a registry for type tag strings and functions to
// encode and decode wrapper objects.
package jsontypes
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
)
// The Tagged interface must be implemented by a type in order to register it
// with the jsontypes package. The TypeTag method returns a string label that
// is used to distinguish objects of that type.
type Tagged interface {
TypeTag() string
}
// registry records the mapping from type tags to value types. Values in this
// map must be normalized to non-pointer types.
var registry = struct {
types map[string]reflect.Type
}{types: make(map[string]reflect.Type)}
// register adds v to the type registry. It reports an error if the tag
// returned by v is already registered.
func register(v Tagged) error {
tag := v.TypeTag()
if t, ok := registry.types[tag]; ok {
return fmt.Errorf("type tag %q already registered to %v", tag, t)
}
typ := reflect.TypeOf(v)
if typ.Kind() == reflect.Ptr {
typ = typ.Elem()
}
registry.types[tag] = typ
return nil
}
// MustRegister adds v to the type registry. It will panic if the tag returned
// by v is already registered. This function is meant for use during program
// initialization.
func MustRegister(v Tagged) {
if err := register(v); err != nil {
panic(err)
}
}
type wrapper struct {
Type string `json:"type"`
Value json.RawMessage `json:"value"`
}
// Marshal marshals a JSON wrapper object containing v. If v == nil, Marshal
// returns the JSON "null" value without error.
func Marshal(v Tagged) ([]byte, error) {
if v == nil {
return []byte("null"), nil
}
data, err := json.Marshal(v)
if err != nil {
return nil, err
}
return json.Marshal(wrapper{
Type: v.TypeTag(),
Value: data,
})
}
// Unmarshal unmarshals a JSON wrapper object into v. It reports an error if
// the data do not encode a valid wrapper object, if the wrapper's type tag is
// not registered with jsontypes, or if the resulting value is not compatible
// with the type of v.
func Unmarshal(data []byte, v interface{}) error {
// Verify that the target is some kind of pointer.
target := reflect.ValueOf(v)
if target.Kind() != reflect.Ptr {
return fmt.Errorf("target %T is not a pointer", v)
}
var w wrapper
dec := json.NewDecoder(bytes.NewReader(data))
dec.DisallowUnknownFields()
if err := dec.Decode(&w); err != nil {
return fmt.Errorf("invalid type wrapper: %w", err)
}
typ, ok := registry.types[w.Type]
if !ok {
return fmt.Errorf("unknown type tag: %q", w.Type)
} else if !typ.AssignableTo(target.Elem().Type()) {
return fmt.Errorf("type %v not assignable to %T", typ, v)
}
obj := reflect.New(typ)
if err := json.Unmarshal(w.Value, obj.Interface()); err != nil {
return fmt.Errorf("decoding wrapped value: %w", err)
}
target.Elem().Set(obj.Elem())
return nil
}
+83
View File
@@ -0,0 +1,83 @@
package jsontypes_test
import (
"testing"
"github.com/tendermint/tendermint/internal/jsontypes"
)
type testType struct {
Field string `json:"field"`
}
func (*testType) TypeTag() string { return "test/TaggedType" }
func TestRoundTrip(t *testing.T) {
const wantEncoded = `{"type":"test/TaggedType","value":{"field":"hello"}}`
t.Run("MustRegisterOK", func(t *testing.T) {
defer func() {
if x := recover(); x != nil {
t.Fatalf("Registration panicked: %v", x)
}
}()
jsontypes.MustRegister((*testType)(nil))
})
t.Run("MustRegisterFail", func(t *testing.T) {
defer func() {
if x := recover(); x != nil {
t.Logf("Got expected panic: %v", x)
}
}()
jsontypes.MustRegister((*testType)(nil))
t.Fatal("Registration should not have succeeded")
})
t.Run("MarshalNil", func(t *testing.T) {
bits, err := jsontypes.Marshal(nil)
if err != nil {
t.Fatalf("Marshal failed: %v", err)
}
if got := string(bits); got != "null" {
t.Errorf("Marshal nil: got %#q, want null", got)
}
})
t.Run("RoundTrip", func(t *testing.T) {
obj := testType{Field: "hello"}
bits, err := jsontypes.Marshal(&obj)
if err != nil {
t.Fatalf("Marshal %T failed: %v", obj, err)
}
if got := string(bits); got != wantEncoded {
t.Errorf("Marshal %T: got %#q, want %#q", obj, got, wantEncoded)
}
var cmp testType
if err := jsontypes.Unmarshal(bits, &cmp); err != nil {
t.Errorf("Unmarshal %#q failed: %v", string(bits), err)
}
if obj != cmp {
t.Errorf("Unmarshal %#q: got %+v, want %+v", string(bits), cmp, obj)
}
})
t.Run("Unregistered", func(t *testing.T) {
obj := testType{Field: "hello"}
bits, err := jsontypes.Marshal(&obj)
if err != nil {
t.Fatalf("Marshal %T failed: %v", obj, err)
}
if got := string(bits); got != wantEncoded {
t.Errorf("Marshal %T: got %#q, want %#q", obj, got, wantEncoded)
}
var cmp struct {
Field string `json:"field"`
}
if err := jsontypes.Unmarshal(bits, &cmp); err != nil {
t.Errorf("Unmarshal %#q: got %+v, want %+v", string(bits), cmp, obj)
}
})
}
@@ -52,6 +52,7 @@ func (pk privKeyWithNilPubKey) Sign(msg []byte) ([]byte, error) { return pk.orig
func (pk privKeyWithNilPubKey) PubKey() crypto.PubKey { return nil }
func (pk privKeyWithNilPubKey) Equals(pk2 crypto.PrivKey) bool { return pk.orig.Equals(pk2) }
func (pk privKeyWithNilPubKey) Type() string { return "privKeyWithNilPubKey" }
func (privKeyWithNilPubKey) TypeTag() string { return "test/privKeyWithNilPubKey" }
func TestSecretConnectionHandshake(t *testing.T) {
fooSecConn, barSecConn := makeSecretConnPair(t)