-
Notifications
You must be signed in to change notification settings - Fork 20.3k
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
Conversation
p2p/enr/enr.go
Outdated
return nil | ||
} | ||
|
||
func wrapList(c []byte) []byte { |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this 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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can use btcec.Signature.Verify
instead: https://godoc.org/github.com/btcsuite/btcd/btcec#example-package--VerifySignature
p2p/enr/enr.go
Outdated
return signature, raw, nil | ||
} | ||
|
||
func (e *ENR) SerialisedContent() ([]byte, error) { |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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)
Something I forgot in the review summary earlier: the terms are slightly off in your implementation. |
@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. |
There was a problem hiding this 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.
p2p/enr/discv5.go
Outdated
"github.com/ethereum/go-ethereum/rlp" | ||
) | ||
|
||
type DiscV5 uint32 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
UDP ports are uint16.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
p2p/enr/discv5.go
Outdated
return err | ||
} | ||
return nil | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please rename DiscV5
-> DiscPort
.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
p2p/enr/secp256k1.go
Outdated
return err | ||
} | ||
return nil | ||
} |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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))
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
For your convenience, output of |
I'll re-review when the rest of the boxes are checked ;) |
@fjl great, will ping you when the TODO list is addressed. |
@fjl @zsfelfoldi I think I addressed all comments and TODOs. This should be ready for review now. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
excellent
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
excellent
…instead of crypto
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.
There was a problem hiding this 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 { |
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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" } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
@zsfelfoldi I think I addressed your comments. @fjl please review the |
Please bump the copyright headers to 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. |
Initial implementation of ENR according to ethereum/EIPs#778
initial implementation of ENR according to ethereum/EIPs#778
TODO:
r.raw
on RLP decode, so that record is ready for RLP encoding after RLP decoding.pairs
on Set, use binary search onLoad
pair.value
- once inSet()
and once issignAndEncode()
Set