Skip to content

Commit 7c0580f

Browse files
committed
Add handshake codec
The goal of the this codec is to serialize `params.UpgradeConfig` in a deterministic way, to be hashed later. This hash is going to be share by nodes at handshake, so they can determine if they have the same upgrade config. It has to be deterministic, otherwise same configs may have different hashes, making them believe they are on different configs. This attempt leverages JSON to serialize the `params.UpgradeConfig`, but using `sortedMarshal` which walks over the object using reflection. Each object is sorted by their key, any scalar value is just serialized with regular JSON. Because the objects are sorted, `{z: 1, b: 2}` and `{b: 2, z:1}` are going to be identical when hashing. The ideal solution would involve writing our own `codec`, based on `reflectcodec`, but extending it so the structs are sorted by their keys, even nested structs. Another important thing that we should support is that this new codec that is being proposed should accept properties with the `json:` or `serialize:` annotation, because we're using existing structs that are defined and maintined outside of this context. Right now JSON is being used inside `UpgradeConfigInternal` to bypass that limitation
1 parent dc9cc5c commit 7c0580f

File tree

4 files changed

+187
-0
lines changed

4 files changed

+187
-0
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// (c) 2019-2023, Ava Labs, Inc. All rights reserved.
2+
// See the file LICENSE for licensing terms.
3+
4+
package handshake
5+
6+
import (
7+
"github.com/ava-labs/avalanchego/codec"
8+
"github.com/ava-labs/avalanchego/codec/linearcodec"
9+
"github.com/ava-labs/avalanchego/utils/units"
10+
"github.com/ava-labs/avalanchego/utils/wrappers"
11+
)
12+
13+
const (
14+
Version = uint16(0)
15+
maxMessageSize = 1 * units.MiB
16+
)
17+
18+
var (
19+
Codec codec.Manager
20+
)
21+
22+
func init() {
23+
Codec = codec.NewManager(maxMessageSize)
24+
c := linearcodec.NewDefault()
25+
26+
errs := wrappers.Errs{}
27+
errs.Add(
28+
c.RegisterType(UpgradeConfigInternal{}),
29+
30+
Codec.RegisterCodec(Version, c),
31+
)
32+
33+
if errs.Errored() {
34+
panic(errs.Err)
35+
}
36+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package handshake
2+
3+
import (
4+
"encoding/json"
5+
"reflect"
6+
"sort"
7+
)
8+
9+
func sortedMarshal(v interface{}) ([]byte, error) {
10+
rv := reflect.ValueOf(v)
11+
12+
if rv.Kind() == reflect.Ptr {
13+
rv = rv.Elem()
14+
}
15+
16+
switch rv.Kind() {
17+
case reflect.Struct:
18+
type field struct {
19+
Name string
20+
Value interface{}
21+
}
22+
23+
var fields []field
24+
25+
for i := 0; i < rv.NumField(); i++ {
26+
Name := rv.Type().Field(i).Name
27+
Value := rv.Field(i).Interface()
28+
fields = append(fields, field{Name, Value})
29+
}
30+
31+
sort.Slice(fields, func(i, j int) bool {
32+
return fields[i].Name < fields[j].Name
33+
})
34+
35+
sortedMap := make(map[string]interface{})
36+
for _, f := range fields {
37+
if f.Value != nil && reflect.ValueOf(f.Value).Kind() == reflect.Struct {
38+
sortedNested, err := sortedMarshal(f.Value)
39+
if err != nil {
40+
return nil, err
41+
}
42+
sortedMap[f.Name] = json.RawMessage(sortedNested)
43+
} else {
44+
sortedMap[f.Name] = f.Value
45+
}
46+
}
47+
48+
return json.Marshal(sortedMap)
49+
default:
50+
// There is nothing to sort for non struct
51+
return json.Marshal(v)
52+
}
53+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package handshake
2+
3+
import (
4+
"crypto/sha256"
5+
"encoding/json"
6+
"fmt"
7+
8+
"github.com/ava-labs/subnet-evm/params"
9+
)
10+
11+
type UpgradeConfigInternal struct {
12+
Bytes []byte `serialize:"true"`
13+
}
14+
15+
type UpgradeConfig struct {
16+
config params.UpgradeConfig
17+
bytes []byte
18+
}
19+
20+
func ParseUpgradeConfig(bytes []byte) (*UpgradeConfig, error) {
21+
var internal UpgradeConfigInternal
22+
version, err := Codec.Unmarshal(bytes, &internal)
23+
if err != nil {
24+
return nil, err
25+
}
26+
if version != Version {
27+
return nil, fmt.Errorf("Invalid version")
28+
}
29+
30+
var config params.UpgradeConfig
31+
32+
if err := json.Unmarshal(internal.Bytes, &config); err != nil {
33+
return nil, err
34+
}
35+
36+
return &UpgradeConfig{config, bytes}, nil
37+
}
38+
39+
func NewUpgradeConfig(config params.UpgradeConfig) (*UpgradeConfig, error) {
40+
Bytes, err := sortedMarshal(config)
41+
if err != nil {
42+
return nil, err
43+
}
44+
instance := UpgradeConfigInternal{Bytes}
45+
bytes, err := Codec.Marshal(Version, instance)
46+
if err != nil {
47+
return nil, err
48+
}
49+
50+
return &UpgradeConfig{config, bytes}, nil
51+
}
52+
53+
func (r *UpgradeConfig) Config() params.UpgradeConfig {
54+
return r.config
55+
}
56+
57+
func (r *UpgradeConfig) Bytes() []byte {
58+
return r.bytes
59+
}
60+
61+
func (r *UpgradeConfig) Hash() [32]byte {
62+
return sha256.Sum256(r.bytes)
63+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package handshake
2+
3+
import (
4+
"testing"
5+
6+
"github.com/ava-labs/subnet-evm/params"
7+
"github.com/ava-labs/subnet-evm/precompile/contracts/nativeminter"
8+
"github.com/ethereum/go-ethereum/common"
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestSerialize(t *testing.T) {
13+
config, err := NewUpgradeConfig(params.UpgradeConfig{
14+
PrecompileUpgrades: []params.PrecompileUpgrade{
15+
{
16+
nativeminter.NewConfig(common.Big0, nil, nil, nil), // enable at genesis
17+
},
18+
{
19+
nativeminter.NewDisableConfig(common.Big1), // disable at timestamp 1
20+
},
21+
},
22+
})
23+
assert.NoError(t, err)
24+
25+
config2, err := ParseUpgradeConfig(config.Bytes())
26+
assert.NoError(t, err)
27+
28+
config3, err := NewUpgradeConfig(config2.Config())
29+
assert.NoError(t, err)
30+
31+
assert.Equal(t, config, config2)
32+
assert.Equal(t, config, config3)
33+
assert.Equal(t, config.Hash(), config2.Hash())
34+
assert.Equal(t, config.Hash(), config3.Hash())
35+
}

0 commit comments

Comments
 (0)