-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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
tlv: add library for new message/payload serialization format #3061
Conversation
6ef4874
to
5fcad7f
Compare
Nice work in this PR. It shows careful engineering. |
panic(ErrNoDecoder.Error()) | ||
} | ||
if record.typ == math.MaxUint64 { | ||
overflow = true |
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.
If the final type is what overflows, then it appears we won't panic at all?
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.
if the final type is math.MaxUint64
that's okay, so long as there isn't a number after it. overflow
in this case signifies that a record hit math.MaxUint64
, and any other records should be forbidden
tlv/stream.go
Outdated
panic(ErrNoEncoder.Error()) | ||
} | ||
if record.decoder == nil { | ||
panic(ErrNoDecoder.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.
Depending on the context in which this is used within lnd
itself, we may actually want to return an error instead.
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.
agreed, i'll change this to use Must
paradigm employed by the go std library
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.
hmm, got a better name than tlv.MustStream
?
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.
MustNewStream
? lol
Either one works IMO. Must
can can just wrap the regular and panic instead of panicing.
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.
What about the case where we only decode a stream and don't have an encoder available. Should record.encoder
be set to a dummy encoder? (same question for encode-only)
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.
Also I actually rely on the decoder
being nil for my TLV-EOB PR within the router. When we receive a fully serialized TLV record stream over the RPC from the user (custom route for SendToRoute
) I attach a shim encoder that just writes the raw bytes, and I have no need for a decoder since we'll never decode into the record. As a work around, I guess I can provide one that just does nothing instead since it's a special case.
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.
perhaps instead we could make a nop encoder/decoder and automatically set them if no encoder or decoder is provided
// inspect each record that is parsed and check to see if it has a corresponding | ||
// Record to facilitate deserialization of that field. If the record is unknown, | ||
// the Stream will discard the record's bytes and proceed to the subsequent | ||
// record. |
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.
If the record is unknown,
// the Stream will discard the record's bytes and proceed to the subsequent
// record.
Is this subject to the even/odd rule? Also perhaps we should allow a "strict" stream that'll always error our rather than silently go to the next record to be decoded.
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.
yes a strict mode would definitely be useful. the original version of this pr included that, but i removed it for now to simplify the diff
// We permit an io.EOF error only when reading the type byte which signals that | ||
// the last record was read cleanly and we should stop parsing. All other io.EOF | ||
// or io.ErrUnexpectedEOF errors are returned. | ||
func (s *Stream) Decode(r io.Reader) 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.
Not a blocker for this PR, but this method would make for a good fuzz testing target. Even before the, we can assert compliance of our encoder by using testing/quick
here to ensure we can always ingest what we produce, and don't inadvertently create any invalid streams.
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.
ooo good idea, maybe we can add it to the fuzzing harnesses that @Crypt-iQ has been working on
Reviewee bump! |
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.
Awesome work, pretty stoked about finally starting to circle in on a sane serialization format! 🤓
// val is not a **btcec.PublicKey. | ||
func EPubKey(w io.Writer, val interface{}, _ *[8]byte) error { | ||
if pk, ok := val.(**btcec.PublicKey); ok { | ||
_, err := w.Write((*pk).SerializeCompressed()) |
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.
Do we really need methods for PublicKey
, or could we just require the caller to use [33]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.
the test vectors in the spec require us to fail when reading an invalid point, so only parsing [33]byte
isn't strict enough. arguably that check could be deferred to a higher level, but it this is the safest approach
8df5fa4
to
2058189
Compare
tlv/stream.go
Outdated
panic(ErrNoEncoder.Error()) | ||
} | ||
if record.decoder == nil { | ||
panic(ErrNoDecoder.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.
MustNewStream
? lol
Either one works IMO. Must
can can just wrap the regular and panic instead of panicing.
// value was not minimally encoded. | ||
var ErrTUintNotMinimal = errors.New("truncated uint not minimally encoded") | ||
|
||
// numLeadingZeroBytes16 computes the number of leading zeros for a 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.
Are there test vectors in the spec for these truncated variants?
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 spec tests the 32 and 64 bit variants, tho not the 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.
Only a few non-blocking questions. LGTM
tlv/stream.go
Outdated
panic(ErrNoEncoder.Error()) | ||
} | ||
if record.decoder == nil { | ||
panic(ErrNoDecoder.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.
What about the case where we only decode a stream and don't have an encoder available. Should record.encoder
be set to a dummy encoder? (same question for encode-only)
|
||
// BenchmarkEncodeCreateSession benchmarks encoding of the non-TLV | ||
// CreateSession. | ||
func BenchmarkEncodeCreateSession(t *testing.B) { |
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 ran the benchmark and observed the difference between tlv and non-tlv. Pretty impressive. I was wondering, would the gap be much wider if the non-tlv case was optimized more? (mainly thinking about also reusing a buffer)
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.
yes there's a good bit of optimization that could be done to non-tlv messages. amongst other things, this was one of the optimizations included in btcsuite/btcd#1426. i don't think we'd see as significant of gains for lnwire
messages since the primary speed up was due to reducing RTTs with the buffer pool, but this certainly helps remove unnecessary allocations and gc pressure
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.
Ok, interesting. Yes, my main thinking was that for a fair comparison, both non-tlv and tlv should be as optimized as possible. But then there is also the "custom tlv encoder/decoder" method, where (unrolled) code is generated for a specific stream. From a theoretical pov it would be nice to see how all three of them compare. What the absolute minimum overhead of tlv is. Definitely not the most important question atm.
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.
agreed, i too would be interested to see that. my goal here was to squeeze some low hanging fruit out of tlv, though as you said the actual overhead might be a little higher if we optimize the traditional encoding. there's also the question of even if those optimizations were possible, would they ever be deployed. if we think tlv will be the base encoding for our messages going forward, we may have to just accept whatever it is :P
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.
LGTM 💿
This varint has the same serialization as the varint in btcd and bitcoind, but has different behavior wrt returned errors. In order to ensure the inner loop properly detects cleanly written records, ReadVarInt will not only return EOF if it can't read the first byte, as that means the reader has zero bytes left. It also modifies the API to allow the caller to provided a static byte array, which can be reused across all encoding and decoding and increases performance.
This commit adds concrete encoding methods for primitive integral types. When external libs need to create custom encoders, this allows them to do so without incurring an extra allocation on the heap. Previously, the need to pass a pointer to the integer using an interface{} would cause the argument to escape, which we avoid by having them copied directly.
This commit adds the truncated integer encodings used in the variable-size onion payloads. The amount and cltv delta both use the truncated encoding to shave bytes in the overall size, and will likely be used in the future for additional extensions where size is a constraint.
Do you think it would be a good idea to include a boolean to the primitives? |
An implementation of lightning/bolts#607