Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add handshake codec #762

Draft
wants to merge 57 commits into
base: master
Choose a base branch
from
Draft

Add handshake codec #762

wants to merge 57 commits into from

Conversation

nytzuga
Copy link
Contributor

@nytzuga nytzuga commented Jul 28, 2023

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 PR leverages the newly introduced support to serilize Maps
ava-labs/avalanchego#1790, which are deterministic (the
values are sorted by keys before serializing)

Why this should be merged

How this works

How this was tested

How is this documented

@nytzuga nytzuga force-pushed the handshake-codec branch 2 times, most recently from 7c0580f to a5266ff Compare July 28, 2023 16:58
@nytzuga
Copy link
Contributor Author

nytzuga commented Jul 28, 2023

I think the main thing to be done is to write our own codec, that should extend reflectcodec (we can even host it in this repo) to sort all the properties inside a Struct before serializing. This sorting is required so, no matter how the struct is defined, we have a pretty standarized way of serializing. The ultimate goal is to have the same binary and hash for the configuration.

The new codec should also serialize anything json:* as well as it does with serialize:"true". Otherwise, all precompiles must have the serialize:"true", which is fine for now, but too error prone for the future.'

Right now this PR works, but it is suboptimal as it uses raw-JSON under the hood. I don't like that.

@ceyonur
Copy link
Collaborator

ceyonur commented Jul 31, 2023

As you mentioned, JSON sorting could be potentially dangerous. GoLang JSON marshalling has a sorting mechanism but that's not in the spec and cannot be relied (it's just a implementation detail and can be changed without backward compatibility). We can still try to get advantage of json tags but using json marshalling seems a bit dangerous.

Some solutions we can use:
1- get advantage of json tags and serialize without explicitly adding new methods/new tags. This would be the most ideal solution since we won't break current precompiles; but also could not be possible to implement and we might don't want to lose time with this one.

2- Break the Config interface in precompile>precompileconfig>config.go by adding encoding.BinaryMarshaler and expect implementation.

  • We should add the implementation to precompile generation tool
  • We can use go:generate to generate the imlementation: https://go.dev/blog/generate

3- Again break the Config interface but use RLP package to generate RLP encoding: https://pkg.go.dev/github.com/ethereum/go-ethereum/rlp. This could be easier than 2nd approach, as we have an implementation here; and it's easier to generate the code through go:generate go run github.com/ethereum/go-ethereum/rlp/rlpgen ....

4- Just encode Upgrade struct in upgradable: https://github.com/ava-labs/subnet-evm/blob/master/precompile/precompileconfig/upgradeable.go#L11. We can use this to understand if precompiles/upgrades were activated at correct timestamp. However this would mean we cannot differentiate the actual configs (admin addresses, initial configs etc).

5- Use the upgrade file bytes directly as it is (no sorting, no conversion). Means that we treat files as the source of truth (rather than using structs). Obviously there could be some problems like whitespacing in files, extra comas, CR/LF style endings etc.

@nytzuga nytzuga self-assigned this Aug 14, 2023
@nytzuga nytzuga marked this pull request as draft August 14, 2023 02:40
@nytzuga nytzuga force-pushed the handshake-codec branch 2 times, most recently from de94050 to 6740f23 Compare August 14, 2023 03:04
@nytzuga nytzuga changed the title Draft: Add handshake codec Add handshake codec Sep 8, 2023
@nytzuga nytzuga marked this pull request as ready for review September 8, 2023 14:48
@nytzuga nytzuga requested a review from anusha-ctrl as a code owner September 8, 2023 14:48
plugin/evm/message/handshake/upgrade_config.go Outdated Show resolved Hide resolved
plugin/evm/message/handshake/upgrade_config.go Outdated Show resolved Hide resolved
plugin/evm/message/handshake/upgrade_config.go Outdated Show resolved Hide resolved
plugin/evm/message/handshake/upgrade_config.go Outdated Show resolved Hide resolved
plugin/evm/message/handshake/codec.go Outdated Show resolved Hide resolved
plugin/evm/message/handshake/upgrade_config.go Outdated Show resolved Hide resolved
"github.com/stretchr/testify/assert"
)

func TestSerialize(t *testing.T) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add more test cases with:

  • Enabled/Admin addresses
  • Initial configs (like mint config)
  • State Upgrades

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to make sure:

  • all of our precompiles can be deserializable
  • add serialize tags to precompile config template
  • if possible add a test to config_test.goso that new precompiles can be tested out when generated.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^^^

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still pending.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm writing more tests now

precompile/contracts/feemanager/config.go Outdated Show resolved Hide resolved
plugin/evm/message/handshake/upgrade_config_test.go Outdated Show resolved Hide resolved
plugin/evm/message/handshake/upgrade_config.go Outdated Show resolved Hide resolved
@nytzuga nytzuga force-pushed the handshake-codec branch 3 times, most recently from 7117339 to 41d7776 Compare September 14, 2023 19:22
@aaronbuchwald aaronbuchwald requested a review from ceyonur October 4, 2023 14:44
params/config.go Outdated Show resolved Hide resolved
// TODO: once we add the first optional upgrade here, we should uncomment TestVMUpgradeBytesOptionalNetworkUpgrades
type OptionalNetworkUpgrades struct{}
type OptionalNetworkUpgrades struct {
Updates []Fork `json:"serialize,omitempty" serialize:"true"`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking this to be something like:

type OptionalNetworkUpgrades struct {
	XYZUpgradeTimestamp *uint64 `json:"xyzUpgradeTimestamp,omitempty"` serialize:"true"
        ...

But I'm not completely against using a list of upgrades here. Seems like with this way we can just include them as elements in a list in the genesis/upgrades to be activated.

However we should not use fork type here as (we would not need optional + block types in the struct). We can define our custom struct to wire this and revert changes to the fork type.

Plus this is still wrong as the json tag is serialize, it should be at least something like json:"optionalNetworkUpgrades,omitempty"

cc @aaronbuchwald @darioush

precompile/contracts/feemanager/config.go Outdated Show resolved Hide resolved
plugin/evm/message/handshake/upgrade_config.go Outdated Show resolved Hide resolved
plugin/evm/message/handshake/upgrade_config.go Outdated Show resolved Hide resolved
plugin/evm/message/handshake/upgrade_config.go Outdated Show resolved Hide resolved
plugin/evm/message/handshake/upgrade_config.go Outdated Show resolved Hide resolved
plugin/evm/message/handshake/upgrade_config.go Outdated Show resolved Hide resolved
plugin/evm/message/handshake/upgrade_config.go Outdated Show resolved Hide resolved
"github.com/stretchr/testify/assert"
)

func TestSerialize(t *testing.T) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^^^

@nytzuga nytzuga force-pushed the handshake-codec branch 5 times, most recently from 5a1af29 to b6c0e8a Compare October 13, 2023 03:33
params/config.go Outdated Show resolved Hide resolved
Use MarshalBinary and UnmarshalBinary
params/config.go Outdated Show resolved Hide resolved
precompile/allowlist/config.go Outdated Show resolved Hide resolved
precompile/allowlist/config.go Outdated Show resolved Hide resolved
Comment on lines 148 to 153
func (c *FeeConfig) getBigIntToSerialize() []**big.Int {
return []**big.Int{
&c.GasLimit, &c.MinBaseFee, &c.TargetGas, &c.BaseFeeChangeDenominator,
&c.MinBlockGasCost, &c.MaxBlockGasCost, &c.BlockGasCostStep,
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see this is trying to make the code simpler by passing a list of pointers to pointers so we can serialize and de-serialize with a for loop instead of one by one.

It's not valid for any of these to be nil, so would it be possible to remove the double pointer and enforce that all of them are non-nil instead of allowing a boolean to indicate whether or not it's nil?

commontype/fee_config.go Outdated Show resolved Hide resolved
params/config.go Outdated
Comment on lines 1032 to 1041
bytes, err := originalConfig.MarshalBinary()
require.NoError(t, err)

deserializedConfig := UpgradeConfig{}
require.NoError(t, deserializedConfig.UnmarshalBinary(bytes))

twiceDeserialized := UpgradeConfig{}
newBytes, err := deserializedConfig.MarshalBinary()
require.NoError(t, err)
require.NoError(t, twiceDeserialized.UnmarshalBinary(newBytes))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When will this fail because we do it three times instead of two?

If newBytes is the same as bytes (which should be confirmed by checking that the hashes match), then I don't think that twiceDeserialized provides additional coverage. Afaict this will fail if UnmarshalBinary is itself non-deterministic on the SAME bytes.

precompile/contracts/nativeminter/config.go Outdated Show resolved Hide resolved
},
},
}
params.AssertConfigHashesAndSerialization(t, &config)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add an expected hash to these tests, so that the tests will fail if the format changes unexpectedly? (this has the cost of needing to update the expected hash values if we change the format, so may be easiest to make this a follow up)

config := params.UpgradeConfig{
PrecompileUpgrades: []params.PrecompileUpgrade{
{
Config: NewConfig(&t0, []common.Address{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add a test with multiple addresses?

if p.Err != nil {
return p.Err
}
u.RewardAddress = common.BytesToAddress(p.UnpackBytes())
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we use fixed bytes of length 20?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in e079719

{
BlockTimestamp: &t0,
StateUpgradeAccounts: map[common.Address]StateUpgradeAccount{
common.BytesToAddress(common.Hex2Bytes("0000000000000000000000000000000000000000000000000000000000000050")): StateUpgradeAccount{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can we create a variable for these addresses instead of hardcoding them into tests?

precompile/allowlist/config.go Outdated Show resolved Hide resolved
// hashing, depite maybe not being identical (some configurations may be in a
// different order, but our hashing algorithm is resilient to those changes,
// thanks for our serialization library, which produces always the same output.
func AssertConfigHashesAndSerialization(t *testing.T, originalConfig *UpgradeConfig) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requiring this function from params package in precompiles will create a cyclic dependency most likely. I think this assertion can be reduced as suggested. So we can just readd these assertions in relevant test files without requiring to import from params package.

params/config.go Outdated Show resolved Hide resolved
@@ -48,6 +54,14 @@ func NewConfig(blockTimestamp *uint64{{if .Contract.AllowList}}, admins []common
}
}

func (c * Config) MarshalBinary() ([]byte, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add some comments here why these are important, how these can be implemented etc?

@nytzuga nytzuga marked this pull request as draft January 4, 2024 14:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: In Progress 🏗
Status: In Review 👀
Development

Successfully merging this pull request may close these issues.

3 participants