Skip to content

Commit dbe2146

Browse files
author
M. J. Fromberger
authored
rpc: simplify the encoding of interface-typed arguments in JSON (tendermint#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.
1 parent 7ed57ef commit dbe2146

File tree

19 files changed

+349
-24
lines changed

19 files changed

+349
-24
lines changed

crypto/crypto.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package crypto
22

33
import (
44
"github.com/tendermint/tendermint/crypto/tmhash"
5+
"github.com/tendermint/tendermint/internal/jsontypes"
56
"github.com/tendermint/tendermint/libs/bytes"
67
)
78

@@ -25,6 +26,9 @@ type PubKey interface {
2526
VerifySignature(msg []byte, sig []byte) bool
2627
Equals(PubKey) bool
2728
Type() string
29+
30+
// Implementations must support tagged encoding in JSON.
31+
jsontypes.Tagged
2832
}
2933

3034
type PrivKey interface {
@@ -33,6 +37,9 @@ type PrivKey interface {
3337
PubKey() PubKey
3438
Equals(PrivKey) bool
3539
Type() string
40+
41+
// Implementations must support tagged encoding in JSON.
42+
jsontypes.Tagged
3643
}
3744

3845
type Symmetric interface {

crypto/ed25519/ed25519.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212

1313
"github.com/tendermint/tendermint/crypto"
1414
"github.com/tendermint/tendermint/crypto/tmhash"
15+
"github.com/tendermint/tendermint/internal/jsontypes"
1516
tmjson "github.com/tendermint/tendermint/libs/json"
1617
)
1718

@@ -58,11 +59,17 @@ const (
5859
func init() {
5960
tmjson.RegisterType(PubKey{}, PubKeyName)
6061
tmjson.RegisterType(PrivKey{}, PrivKeyName)
62+
63+
jsontypes.MustRegister(PubKey{})
64+
jsontypes.MustRegister(PrivKey{})
6165
}
6266

6367
// PrivKey implements crypto.PrivKey.
6468
type PrivKey []byte
6569

70+
// TypeTag satisfies the jsontypes.Tagged interface.
71+
func (PrivKey) TypeTag() string { return PrivKeyName }
72+
6673
// Bytes returns the privkey byte format.
6774
func (privKey PrivKey) Bytes() []byte {
6875
return []byte(privKey)
@@ -151,6 +158,9 @@ var _ crypto.PubKey = PubKey{}
151158
// PubKeyEd25519 implements crypto.PubKey for the Ed25519 signature scheme.
152159
type PubKey []byte
153160

161+
// TypeTag satisfies the jsontypes.Tagged interface.
162+
func (PubKey) TypeTag() string { return PubKeyName }
163+
154164
// Address is the SHA256-20 of the raw pubkey bytes.
155165
func (pubKey PubKey) Address() crypto.Address {
156166
if len(pubKey) != PubKeySize {

crypto/secp256k1/secp256k1.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
secp256k1 "github.com/btcsuite/btcd/btcec"
1212
"github.com/tendermint/tendermint/crypto"
13+
"github.com/tendermint/tendermint/internal/jsontypes"
1314
tmjson "github.com/tendermint/tendermint/libs/json"
1415

1516
// necessary for Bitcoin address format
@@ -28,13 +29,19 @@ const (
2829
func init() {
2930
tmjson.RegisterType(PubKey{}, PubKeyName)
3031
tmjson.RegisterType(PrivKey{}, PrivKeyName)
32+
33+
jsontypes.MustRegister(PubKey{})
34+
jsontypes.MustRegister(PrivKey{})
3135
}
3236

3337
var _ crypto.PrivKey = PrivKey{}
3438

3539
// PrivKey implements PrivKey.
3640
type PrivKey []byte
3741

42+
// TypeTag satisfies the jsontypes.Tagged interface.
43+
func (PrivKey) TypeTag() string { return PrivKeyName }
44+
3845
// Bytes marshalls the private key using amino encoding.
3946
func (privKey PrivKey) Bytes() []byte {
4047
return []byte(privKey)
@@ -138,6 +145,9 @@ const PubKeySize = 33
138145
// This prefix is followed with the x-coordinate.
139146
type PubKey []byte
140147

148+
// TypeTag satisfies the jsontypes.Tagged interface.
149+
func (PubKey) TypeTag() string { return PubKeyName }
150+
141151
// Address returns a Bitcoin style addresses: RIPEMD160(SHA256(pubkey))
142152
func (pubKey PubKey) Address() crypto.Address {
143153
if len(pubKey) != PubKeySize {

crypto/sr25519/encoding.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package sr25519
22

3-
import tmjson "github.com/tendermint/tendermint/libs/json"
3+
import (
4+
"github.com/tendermint/tendermint/internal/jsontypes"
5+
tmjson "github.com/tendermint/tendermint/libs/json"
6+
)
47

58
const (
69
PrivKeyName = "tendermint/PrivKeySr25519"
@@ -10,4 +13,7 @@ const (
1013
func init() {
1114
tmjson.RegisterType(PubKey{}, PubKeyName)
1215
tmjson.RegisterType(PrivKey{}, PrivKeyName)
16+
17+
jsontypes.MustRegister(PubKey{})
18+
jsontypes.MustRegister(PrivKey{})
1319
}

crypto/sr25519/privkey.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ type PrivKey struct {
2929
kp *sr25519.KeyPair
3030
}
3131

32+
// TypeTag satisfies the jsontypes.Tagged interface.
33+
func (PrivKey) TypeTag() string { return PrivKeyName }
34+
3235
// Bytes returns the byte-encoded PrivKey.
3336
func (privKey PrivKey) Bytes() []byte {
3437
if privKey.kp == nil {

crypto/sr25519/pubkey.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ const (
2323
// PubKey implements crypto.PubKey.
2424
type PubKey []byte
2525

26+
// TypeTag satisfies the jsontypes.Tagged interface.
27+
func (PubKey) TypeTag() string { return PubKeyName }
28+
2629
// Address is the SHA256-20 of the raw pubkey bytes.
2730
func (pubKey PubKey) Address() crypto.Address {
2831
if len(pubKey) != PubKeySize {

internal/jsontypes/jsontypes.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// Package jsontypes supports decoding for interface types whose concrete
2+
// implementations need to be stored as JSON. To do this, concrete values are
3+
// packaged in wrapper objects having the form:
4+
//
5+
// {
6+
// "type": "<type-tag>",
7+
// "value": <json-encoding-of-value>
8+
// }
9+
//
10+
// This package provides a registry for type tag strings and functions to
11+
// encode and decode wrapper objects.
12+
package jsontypes
13+
14+
import (
15+
"bytes"
16+
"encoding/json"
17+
"fmt"
18+
"reflect"
19+
)
20+
21+
// The Tagged interface must be implemented by a type in order to register it
22+
// with the jsontypes package. The TypeTag method returns a string label that
23+
// is used to distinguish objects of that type.
24+
type Tagged interface {
25+
TypeTag() string
26+
}
27+
28+
// registry records the mapping from type tags to value types. Values in this
29+
// map must be normalized to non-pointer types.
30+
var registry = struct {
31+
types map[string]reflect.Type
32+
}{types: make(map[string]reflect.Type)}
33+
34+
// register adds v to the type registry. It reports an error if the tag
35+
// returned by v is already registered.
36+
func register(v Tagged) error {
37+
tag := v.TypeTag()
38+
if t, ok := registry.types[tag]; ok {
39+
return fmt.Errorf("type tag %q already registered to %v", tag, t)
40+
}
41+
typ := reflect.TypeOf(v)
42+
if typ.Kind() == reflect.Ptr {
43+
typ = typ.Elem()
44+
}
45+
registry.types[tag] = typ
46+
return nil
47+
}
48+
49+
// MustRegister adds v to the type registry. It will panic if the tag returned
50+
// by v is already registered. This function is meant for use during program
51+
// initialization.
52+
func MustRegister(v Tagged) {
53+
if err := register(v); err != nil {
54+
panic(err)
55+
}
56+
}
57+
58+
type wrapper struct {
59+
Type string `json:"type"`
60+
Value json.RawMessage `json:"value"`
61+
}
62+
63+
// Marshal marshals a JSON wrapper object containing v. If v == nil, Marshal
64+
// returns the JSON "null" value without error.
65+
func Marshal(v Tagged) ([]byte, error) {
66+
if v == nil {
67+
return []byte("null"), nil
68+
}
69+
data, err := json.Marshal(v)
70+
if err != nil {
71+
return nil, err
72+
}
73+
return json.Marshal(wrapper{
74+
Type: v.TypeTag(),
75+
Value: data,
76+
})
77+
}
78+
79+
// Unmarshal unmarshals a JSON wrapper object into v. It reports an error if
80+
// the data do not encode a valid wrapper object, if the wrapper's type tag is
81+
// not registered with jsontypes, or if the resulting value is not compatible
82+
// with the type of v.
83+
func Unmarshal(data []byte, v interface{}) error {
84+
// Verify that the target is some kind of pointer.
85+
target := reflect.ValueOf(v)
86+
if target.Kind() != reflect.Ptr {
87+
return fmt.Errorf("target %T is not a pointer", v)
88+
}
89+
90+
var w wrapper
91+
dec := json.NewDecoder(bytes.NewReader(data))
92+
dec.DisallowUnknownFields()
93+
if err := dec.Decode(&w); err != nil {
94+
return fmt.Errorf("invalid type wrapper: %w", err)
95+
}
96+
typ, ok := registry.types[w.Type]
97+
if !ok {
98+
return fmt.Errorf("unknown type tag: %q", w.Type)
99+
} else if !typ.AssignableTo(target.Elem().Type()) {
100+
return fmt.Errorf("type %v not assignable to %T", typ, v)
101+
}
102+
103+
obj := reflect.New(typ)
104+
if err := json.Unmarshal(w.Value, obj.Interface()); err != nil {
105+
return fmt.Errorf("decoding wrapped value: %w", err)
106+
}
107+
target.Elem().Set(obj.Elem())
108+
return nil
109+
}

internal/jsontypes/jsontypes_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package jsontypes_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/tendermint/tendermint/internal/jsontypes"
7+
)
8+
9+
type testType struct {
10+
Field string `json:"field"`
11+
}
12+
13+
func (*testType) TypeTag() string { return "test/TaggedType" }
14+
15+
func TestRoundTrip(t *testing.T) {
16+
const wantEncoded = `{"type":"test/TaggedType","value":{"field":"hello"}}`
17+
18+
t.Run("MustRegisterOK", func(t *testing.T) {
19+
defer func() {
20+
if x := recover(); x != nil {
21+
t.Fatalf("Registration panicked: %v", x)
22+
}
23+
}()
24+
jsontypes.MustRegister((*testType)(nil))
25+
})
26+
27+
t.Run("MustRegisterFail", func(t *testing.T) {
28+
defer func() {
29+
if x := recover(); x != nil {
30+
t.Logf("Got expected panic: %v", x)
31+
}
32+
}()
33+
jsontypes.MustRegister((*testType)(nil))
34+
t.Fatal("Registration should not have succeeded")
35+
})
36+
37+
t.Run("MarshalNil", func(t *testing.T) {
38+
bits, err := jsontypes.Marshal(nil)
39+
if err != nil {
40+
t.Fatalf("Marshal failed: %v", err)
41+
}
42+
if got := string(bits); got != "null" {
43+
t.Errorf("Marshal nil: got %#q, want null", got)
44+
}
45+
})
46+
47+
t.Run("RoundTrip", func(t *testing.T) {
48+
obj := testType{Field: "hello"}
49+
bits, err := jsontypes.Marshal(&obj)
50+
if err != nil {
51+
t.Fatalf("Marshal %T failed: %v", obj, err)
52+
}
53+
if got := string(bits); got != wantEncoded {
54+
t.Errorf("Marshal %T: got %#q, want %#q", obj, got, wantEncoded)
55+
}
56+
57+
var cmp testType
58+
if err := jsontypes.Unmarshal(bits, &cmp); err != nil {
59+
t.Errorf("Unmarshal %#q failed: %v", string(bits), err)
60+
}
61+
if obj != cmp {
62+
t.Errorf("Unmarshal %#q: got %+v, want %+v", string(bits), cmp, obj)
63+
}
64+
})
65+
66+
t.Run("Unregistered", func(t *testing.T) {
67+
obj := testType{Field: "hello"}
68+
bits, err := jsontypes.Marshal(&obj)
69+
if err != nil {
70+
t.Fatalf("Marshal %T failed: %v", obj, err)
71+
}
72+
if got := string(bits); got != wantEncoded {
73+
t.Errorf("Marshal %T: got %#q, want %#q", obj, got, wantEncoded)
74+
}
75+
76+
var cmp struct {
77+
Field string `json:"field"`
78+
}
79+
if err := jsontypes.Unmarshal(bits, &cmp); err != nil {
80+
t.Errorf("Unmarshal %#q: got %+v, want %+v", string(bits), cmp, obj)
81+
}
82+
})
83+
}

internal/p2p/conn/secret_connection_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ func (pk privKeyWithNilPubKey) Sign(msg []byte) ([]byte, error) { return pk.orig
5252
func (pk privKeyWithNilPubKey) PubKey() crypto.PubKey { return nil }
5353
func (pk privKeyWithNilPubKey) Equals(pk2 crypto.PrivKey) bool { return pk.orig.Equals(pk2) }
5454
func (pk privKeyWithNilPubKey) Type() string { return "privKeyWithNilPubKey" }
55+
func (privKeyWithNilPubKey) TypeTag() string { return "test/privKeyWithNilPubKey" }
5556

5657
func TestSecretConnectionHandshake(t *testing.T) {
5758
fooSecConn, barSecConn := makeSecretConnPair(t)

privval/file.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/tendermint/tendermint/crypto"
1515
"github.com/tendermint/tendermint/crypto/ed25519"
1616
"github.com/tendermint/tendermint/crypto/secp256k1"
17+
"github.com/tendermint/tendermint/internal/jsontypes"
1718
"github.com/tendermint/tendermint/internal/libs/protoio"
1819
"github.com/tendermint/tendermint/internal/libs/tempfile"
1920
tmbytes "github.com/tendermint/tendermint/libs/bytes"
@@ -55,6 +56,22 @@ type FilePVKey struct {
5556
filePath string
5657
}
5758

59+
func (pvKey FilePVKey) MarshalJSON() ([]byte, error) {
60+
pubk, err := jsontypes.Marshal(pvKey.PubKey)
61+
if err != nil {
62+
return nil, err
63+
}
64+
privk, err := jsontypes.Marshal(pvKey.PrivKey)
65+
if err != nil {
66+
return nil, err
67+
}
68+
return json.Marshal(struct {
69+
Address types.Address `json:"address"`
70+
PubKey json.RawMessage `json:"pub_key"`
71+
PrivKey json.RawMessage `json:"priv_key"`
72+
}{Address: pvKey.Address, PubKey: pubk, PrivKey: privk})
73+
}
74+
5875
// Save persists the FilePVKey to its filePath.
5976
func (pvKey FilePVKey) Save() error {
6077
outFile := pvKey.filePath

0 commit comments

Comments
 (0)