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

p2p/enr: initial implementation #15585

Merged
merged 35 commits into from
Dec 29, 2017
Merged

p2p/enr: initial implementation #15585

merged 35 commits into from
Dec 29, 2017

Conversation

nonsense
Copy link
Member

@nonsense nonsense commented Nov 30, 2017

initial implementation of ENR according to ethereum/EIPs#778

TODO:

  • compare RLP encoding/decoding with Python impl at https://github.com/fjl/p2p-drafts.
  • reject records longer than 300 bytes.
  • set r.raw on RLP decode, so that record is ready for RLP encoding after RLP decoding.
  • sort pairs on Set, use binary search on Load
  • ensure that setting the same key multiple times will update it instead of including it twice.
  • get rid of double-RLP encoding of pair.value - once in Set() and once is signAndEncode()
  • add documentation
  • use binary search on Set
  • add more tests to check decode/encode and sorting

p2p/enr/enr.go Outdated
return nil
}

func wrapList(c []byte) []byte {
Copy link
Member Author

Choose a reason for hiding this comment

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

This feels like a hack. I am looking to see if there is a nicer way to wrap a list.

p2p/enr/enr.go Outdated
}

digest := crypto.Keccak256Hash(content)
pubkey2, err := crypto.Ecrecover(digest.Bytes(), e.signature)
Copy link
Member Author

Choose a reason for hiding this comment

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

The signature returned from the python impl has a length of 64, and recovering of the public key fails.

Copy link
Contributor

@fjl fjl left a comment

Choose a reason for hiding this comment

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

I think the API for keys/values should be different because there can be arbitrary keys used by different parts of the codebase. We need a way to define new keys outside of package enr.

Another thing to keep in mind is that record validity is defined to be signature validity only. So a record containing the "ip4" key with RLP value [1, 2, 3] is still a valid (but useless) record. The valid vs. useless distinction is important.

Here's my API proposal:

package enr

// Key is implemented by known node record key types.
//
// To define a new key that is to be included in a node record,
// create a Go type that satisfies this interface. The type should
// also implement rlp.Decoder if additional checks are needed on the value.
type Key interface{
	ENRKey() string
}

// IP4 represents a 4-byte IPv4 address in a node record.
type IP4 net.IP

// ENRKey returns the node record key for an IPv4 address.
func (IP4) ENRKey() string {
	return "ip4"
}

func (v IP4) EncodeRLP(w io.Writer) error {
	ip4 := net.IP(v).To4()
	if ip4 == nil {
		return fmt.Errorf("invalid IPv4 address: %v", v)
	}
	return rlp.Encode(w, ip4)
}

func (v *IP4) DecodeRLP(s *rlp.Stream) error {
	if err := s.Decode((*net.IP)(v)); err != nil {
		return err
	}
	if len(*v) != 4 {
		return fmt.Errorf("invalid IPv4 address, want 4 bytes: %v", *v)
	}
	return nil
}

To construct a node record add Keys to the zero value, then sign it.

func settingAndEncoding() {
	// r is an empty record.
	var r enr.Record
	
	// Add basic metadata.
	r.Set(enr.IP4(net.ParseIP("127.0.0.1")))
	r.SetSeq(1)

	// At this point r is non-empty but not signed, so EncodeRLP() returns an error.
	_, err := rlp.EncodeToBytes(r)
	
	// Sign with secp256k1 key. This adds the "id" and "secp256k1" k/v pairs
	// and the signature.
	if err := r.Sign(privkey); err != nil {
		panic(err)
	}
	
	// This works now:
	_, err := rlp.EncodeToBytes(r)

	// Update metadata. This invalidates the signature.
	r.Set(enr.IP4(net.ParseIP("127.0.0.2")))
	r.SetSeq(2)

	// This will return an error again until the record is re-signed.
	_, err := rlp.EncodeToBytes(r)
}

To get a value out of a node record, pass the Key you want.
This will decode the value if present.

func decodingAndAccessing(input []byte) {
	// Decoding into enr.Record validates the signature.
	var r enr.Record
	err := rlp.DecodeBytes(input, &r)
	if err != nil {
		panic(err)
	}

	// You can get any k/v value like this:
	var ip4 enr.IP4
	ok, err := r.Load(&ip4)
	// ip4 contains the address if it was present in the record.
        // ok says whether the key was present, err says whether the
        // value is valid.
	
	// Accessing basic information:
	seq := r.Seq()
	addr := r.NodeAddr()
}

p2p/enr/enr.go Outdated
}

digest := crypto.Keccak256Hash(content)
pubkey2, err := crypto.Ecrecover(digest.Bytes(), e.signature)
Copy link
Contributor

Choose a reason for hiding this comment

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

p2p/enr/enr.go Outdated
return signature, raw, nil
}

func (e *ENR) SerialisedContent() ([]byte, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

No need to export this method.

p2p/enr/enr.go Outdated
}
}

return wrapList(buffer.Bytes()), nil
Copy link
Contributor

@fjl fjl Dec 1, 2017

Choose a reason for hiding this comment

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

You can avoid most of the RLP hackery by using higher-level types instead. To encode a record, put elements into []interface{} then call rlp.EncodeToBytes:

list := []interface{}{nil, e.seq}
// add k/v pairs
for _, r := range e.records {
    list = append(list, k, v)
}
// sign the tail of the list
sigcontent, err := rlp.EncodeToBytes(list[1:])
...
// add signature to front
list[0] = sig
record, err := rlp.EncodeToBytes(list)

@fjl
Copy link
Contributor

fjl commented Dec 1, 2017

Something I forgot in the review summary earlier: the terms are slightly off in your implementation.
A "record" is the whole thing. key/value "pairs" are the elements of a "record". This should be reflected in identifiers.

@nonsense
Copy link
Member Author

nonsense commented Dec 1, 2017

@fjl thanks for the comments. The proposed API is indeed better. I think I addressed your comments, however I still haven't confirmed the RLP encoding/decoding vs the Python impl. - there might be some differences there. Will ping you when this is ready for review.

Copy link
Contributor

@fjl fjl left a comment

Choose a reason for hiding this comment

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

This is starting to look really nice. Thank you so much.
I have two general suggestions and a couple of nitpicks below ;)

Please move all predefined keys into one file. Their definitions aren't that large.

pairs is supposed to be sorted by key at encoding time. Maybe insert into the correct place in Set so it will be sorted at all times. If you do this you can use binary search in Load. Please also ensure that setting the same key multiple times will update it instead of including it twice.

"github.com/ethereum/go-ethereum/rlp"
)

type DiscV5 uint32
Copy link
Contributor

Choose a reason for hiding this comment

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

UDP ports are uint16.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

return err
}
return nil
}
Copy link
Contributor

@fjl fjl Dec 1, 2017

Choose a reason for hiding this comment

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

You can remove rlp encoding methods from Discv5 because the codec 'sees through' named types and encodes them correctly.

Copy link
Contributor

Choose a reason for hiding this comment

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

Please rename DiscV5 -> DiscPort.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

p2p/enr/enr.go Outdated
}

type pair struct {
k []byte
Copy link
Contributor

Choose a reason for hiding this comment

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

Package rlp supports strings. Please make k a string to avoid conversions.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

p2p/enr/id.go Outdated
return err
}
return nil
}
Copy link
Contributor

Choose a reason for hiding this comment

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

You can remove rlp encoding methods from ID.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

return err
}
return nil
}
Copy link
Contributor

@fjl fjl Dec 1, 2017

Choose a reason for hiding this comment

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

Please use

type Secp256k1 ecdsa.PublicKey

You already need to parse the public key for signature verification, might as well do it at decoding time. The additional advantage will be that users can't get invalid curve points by accident.

p2p/enr/enr.go Outdated

id := ID(ID_SECP256k1_KECCAK)

r.Set(id)
Copy link
Contributor

@fjl fjl Dec 1, 2017

Choose a reason for hiding this comment

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

You can be less verbose and write r.Set(ID(SECP256k1_KECCAK)).

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

@fjl
Copy link
Contributor

fjl commented Dec 1, 2017

For your convenience, output of make run from p2p-drafts/testvec should be a lot more helpful now ;).

@fjl
Copy link
Contributor

fjl commented Dec 5, 2017

I'll re-review when the rest of the boxes are checked ;)

@nonsense
Copy link
Member Author

nonsense commented Dec 5, 2017

@fjl great, will ping you when the TODO list is addressed.

@nonsense
Copy link
Member Author

nonsense commented Dec 6, 2017

@fjl @zsfelfoldi I think I addressed all comments and TODOs. This should be ready for review now.

Copy link

@RayPylant RayPylant left a comment

Choose a reason for hiding this comment

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

excellent

Copy link

@RayPylant RayPylant left a comment

Choose a reason for hiding this comment

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

excellent

fjl and others added 17 commits December 20, 2017 14:01
Nobody will ever check the return value of Set. Make it
panic to avoid hidden bugs.
API changes:

- Load no longer returns (bool, error), just error. Missing keys
  can be detected using IsNotFound.
- Load errors contain the key that the caller tried to load.
- NodeAddr doesn't return error anymore. I expect we'll be using this
  method often and it will never return an error for records decoded
  from RLP. Maybe it should panic if "secp256k1" doesn't exist, not sure
  about that yet.
- The Signed method can be used to check for a signature.
Copy link
Contributor

@zsfelfoldi zsfelfoldi left a comment

Choose a reason for hiding this comment

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

Beautiful piece of code :) Left a few comments but nothing critical, LGTM.

p2p/enr/keys.go Outdated
// To define a new key that is to be included in a node record,
// create a Go type that satisfies this interface. The type should
// also implement rlp.Decoder if additional checks are needed on the value.
type Key interface {
Copy link
Contributor

Choose a reason for hiding this comment

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

This name confused me at first, I thought it is just the key from the key/value pair. Not sure what I'd like to suggest instead though, I would probably call it Entry or Item but I am not good at naming stuff :)

Copy link
Member Author

Choose a reason for hiding this comment

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

Entry is not a bad name for this interface. I can see how Key could confuse someone new to the package.

Copy link
Member Author

Choose a reason for hiding this comment

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

I just pushed a commit that renames Key to Entry where I found it to actually mean Entry, rather than key from a key/value pair. I am not 100% convinced the new version is better, probably because I am already used to the Key interface, but I don't have strong opinion on the name.

@fjl @zsfelfoldi let me know what you think, I am fine with both names to be honest.

p2p/enr/enr.go Outdated
// Set adds or updates the given key in the record.
// It panics if the value can't be encoded.
func (r *Record) Set(k Key) {
r.signature = nil
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't this also invalidate r.raw? (probably you are only checking r.signature for nil but maybe it would be nicer to not leave an invalid r.raw there)

Copy link
Member Author

@nonsense nonsense Dec 21, 2017

Choose a reason for hiding this comment

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

I agree - we should invalidate it - someone could try to decode the record, after having set a key and changed its contents. Pushed a change.

p2p/enr/keys.go Outdated
// DiscPort is the "discv5" key, which holds the UDP port for discovery v5.
type DiscPort uint16

func (v DiscPort) ENRKey() string { return "discv5" }
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not just "port" or "udp"? Discovery v5-specific port will go away soon, the UDP port used by discovery will not depend on the version of the discovery protocol so we should not name this field after that.

Copy link
Member Author

Choose a reason for hiding this comment

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

I honestly don't know enough about the roadmap to comment on that. Depending on how soon is soon, and when exactly ENR will be integrated with the rest of the codebase, we can decide whether to keep this DiscPort entry type, or remove it and just add Port.

I guess we can postpone this decision until Discovery v5-specific port actually goes away?

@nonsense
Copy link
Member Author

@zsfelfoldi I think I addressed your comments. @fjl please review the Key rename to Entry and if you are not happy with it, feel free to just revert the commit. I think Entry reflects slightly better the semantics of the interface.

@karalabe
Copy link
Member

Please bump the copyright headers to 2017 :)

@fjl
Copy link
Contributor

fjl commented Dec 21, 2017

The interface is called Key because it assigns the key to a key/value pair (that's what the method returns). I'll know if Entry is right after christmas. It doesn't really matter though.

@fjl fjl merged commit 36a1087 into ethereum:master Dec 29, 2017
@karalabe karalabe added this to the 1.8.0 milestone Dec 29, 2017
mariameda pushed a commit to NiluPlatform/go-nilu that referenced this pull request Aug 23, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants