From 7c0580fc01f110db3d8274ffea166da930deaa9f Mon Sep 17 00:00:00 2001 From: Cesar Date: Fri, 28 Jul 2023 11:14:04 -0400 Subject: [PATCH] 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 --- plugin/evm/message/handshake/codec.go | 36 +++++++++++ .../evm/message/handshake/sorted_marshal.go | 53 ++++++++++++++++ .../evm/message/handshake/upgrade_config.go | 63 +++++++++++++++++++ .../message/handshake/upgrade_config_test.go | 35 +++++++++++ 4 files changed, 187 insertions(+) create mode 100644 plugin/evm/message/handshake/codec.go create mode 100644 plugin/evm/message/handshake/sorted_marshal.go create mode 100644 plugin/evm/message/handshake/upgrade_config.go create mode 100644 plugin/evm/message/handshake/upgrade_config_test.go diff --git a/plugin/evm/message/handshake/codec.go b/plugin/evm/message/handshake/codec.go new file mode 100644 index 0000000000..9b0cfb57d0 --- /dev/null +++ b/plugin/evm/message/handshake/codec.go @@ -0,0 +1,36 @@ +// (c) 2019-2023, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package handshake + +import ( + "github.com/ava-labs/avalanchego/codec" + "github.com/ava-labs/avalanchego/codec/linearcodec" + "github.com/ava-labs/avalanchego/utils/units" + "github.com/ava-labs/avalanchego/utils/wrappers" +) + +const ( + Version = uint16(0) + maxMessageSize = 1 * units.MiB +) + +var ( + Codec codec.Manager +) + +func init() { + Codec = codec.NewManager(maxMessageSize) + c := linearcodec.NewDefault() + + errs := wrappers.Errs{} + errs.Add( + c.RegisterType(UpgradeConfigInternal{}), + + Codec.RegisterCodec(Version, c), + ) + + if errs.Errored() { + panic(errs.Err) + } +} diff --git a/plugin/evm/message/handshake/sorted_marshal.go b/plugin/evm/message/handshake/sorted_marshal.go new file mode 100644 index 0000000000..a37e965d94 --- /dev/null +++ b/plugin/evm/message/handshake/sorted_marshal.go @@ -0,0 +1,53 @@ +package handshake + +import ( + "encoding/json" + "reflect" + "sort" +) + +func sortedMarshal(v interface{}) ([]byte, error) { + rv := reflect.ValueOf(v) + + if rv.Kind() == reflect.Ptr { + rv = rv.Elem() + } + + switch rv.Kind() { + case reflect.Struct: + type field struct { + Name string + Value interface{} + } + + var fields []field + + for i := 0; i < rv.NumField(); i++ { + Name := rv.Type().Field(i).Name + Value := rv.Field(i).Interface() + fields = append(fields, field{Name, Value}) + } + + sort.Slice(fields, func(i, j int) bool { + return fields[i].Name < fields[j].Name + }) + + sortedMap := make(map[string]interface{}) + for _, f := range fields { + if f.Value != nil && reflect.ValueOf(f.Value).Kind() == reflect.Struct { + sortedNested, err := sortedMarshal(f.Value) + if err != nil { + return nil, err + } + sortedMap[f.Name] = json.RawMessage(sortedNested) + } else { + sortedMap[f.Name] = f.Value + } + } + + return json.Marshal(sortedMap) + default: + // There is nothing to sort for non struct + return json.Marshal(v) + } +} diff --git a/plugin/evm/message/handshake/upgrade_config.go b/plugin/evm/message/handshake/upgrade_config.go new file mode 100644 index 0000000000..3341240332 --- /dev/null +++ b/plugin/evm/message/handshake/upgrade_config.go @@ -0,0 +1,63 @@ +package handshake + +import ( + "crypto/sha256" + "encoding/json" + "fmt" + + "github.com/ava-labs/subnet-evm/params" +) + +type UpgradeConfigInternal struct { + Bytes []byte `serialize:"true"` +} + +type UpgradeConfig struct { + config params.UpgradeConfig + bytes []byte +} + +func ParseUpgradeConfig(bytes []byte) (*UpgradeConfig, error) { + var internal UpgradeConfigInternal + version, err := Codec.Unmarshal(bytes, &internal) + if err != nil { + return nil, err + } + if version != Version { + return nil, fmt.Errorf("Invalid version") + } + + var config params.UpgradeConfig + + if err := json.Unmarshal(internal.Bytes, &config); err != nil { + return nil, err + } + + return &UpgradeConfig{config, bytes}, nil +} + +func NewUpgradeConfig(config params.UpgradeConfig) (*UpgradeConfig, error) { + Bytes, err := sortedMarshal(config) + if err != nil { + return nil, err + } + instance := UpgradeConfigInternal{Bytes} + bytes, err := Codec.Marshal(Version, instance) + if err != nil { + return nil, err + } + + return &UpgradeConfig{config, bytes}, nil +} + +func (r *UpgradeConfig) Config() params.UpgradeConfig { + return r.config +} + +func (r *UpgradeConfig) Bytes() []byte { + return r.bytes +} + +func (r *UpgradeConfig) Hash() [32]byte { + return sha256.Sum256(r.bytes) +} diff --git a/plugin/evm/message/handshake/upgrade_config_test.go b/plugin/evm/message/handshake/upgrade_config_test.go new file mode 100644 index 0000000000..f18eb19e4b --- /dev/null +++ b/plugin/evm/message/handshake/upgrade_config_test.go @@ -0,0 +1,35 @@ +package handshake + +import ( + "testing" + + "github.com/ava-labs/subnet-evm/params" + "github.com/ava-labs/subnet-evm/precompile/contracts/nativeminter" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" +) + +func TestSerialize(t *testing.T) { + config, err := NewUpgradeConfig(params.UpgradeConfig{ + PrecompileUpgrades: []params.PrecompileUpgrade{ + { + nativeminter.NewConfig(common.Big0, nil, nil, nil), // enable at genesis + }, + { + nativeminter.NewDisableConfig(common.Big1), // disable at timestamp 1 + }, + }, + }) + assert.NoError(t, err) + + config2, err := ParseUpgradeConfig(config.Bytes()) + assert.NoError(t, err) + + config3, err := NewUpgradeConfig(config2.Config()) + assert.NoError(t, err) + + assert.Equal(t, config, config2) + assert.Equal(t, config, config3) + assert.Equal(t, config.Hash(), config2.Hash()) + assert.Equal(t, config.Hash(), config3.Hash()) +}