Skip to content

Commit

Permalink
Add handshake codec
Browse files Browse the repository at this point in the history
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
  • Loading branch information
nytzuga committed Jul 28, 2023
1 parent dc9cc5c commit 7c0580f
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 0 deletions.
36 changes: 36 additions & 0 deletions plugin/evm/message/handshake/codec.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
53 changes: 53 additions & 0 deletions plugin/evm/message/handshake/sorted_marshal.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
63 changes: 63 additions & 0 deletions plugin/evm/message/handshake/upgrade_config.go
Original file line number Diff line number Diff line change
@@ -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)
}
35 changes: 35 additions & 0 deletions plugin/evm/message/handshake/upgrade_config_test.go
Original file line number Diff line number Diff line change
@@ -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())
}

0 comments on commit 7c0580f

Please sign in to comment.