tlv: add library for new message/payload serialization format#3061
tlv: add library for new message/payload serialization format#3061Roasbeef merged 9 commits intolightningnetwork:masterfrom
Conversation
6ef4874 to
5fcad7f
Compare
|
Nice work in this PR. It shows careful engineering. |
tlv/stream.go
Outdated
There was a problem hiding this comment.
If the final type is what overflows, then it appears we won't panic at all?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
agreed, i'll change this to use Must paradigm employed by the go std library
There was a problem hiding this comment.
hmm, got a better name than tlv.MustStream?
There was a problem hiding this comment.
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.
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.
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.
perhaps instead we could make a nop encoder/decoder and automatically set them if no encoder or decoder is provided
tlv/stream.go
Outdated
There was a problem hiding this comment.
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.
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
tlv/stream.go
Outdated
There was a problem hiding this comment.
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.
ooo good idea, maybe we can add it to the fuzzing harnesses that @Crypt-iQ has been working on
|
Reviewee bump! |
halseth
left a comment
There was a problem hiding this comment.
Awesome work, pretty stoked about finally starting to circle in on a sane serialization format! 🤓
tlv/primitive.go
Outdated
There was a problem hiding this comment.
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.
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
There was a problem hiding this comment.
MustNewStream? lol
Either one works IMO. Must can can just wrap the regular and panic instead of panicing.
tlv/truncated.go
Outdated
There was a problem hiding this comment.
Are there test vectors in the spec for these truncated variants?
There was a problem hiding this comment.
the spec tests the 32 and 64 bit variants, tho not the uint16
joostjager
left a comment
There was a problem hiding this comment.
Only a few non-blocking questions. LGTM
tlv/stream.go
Outdated
There was a problem hiding this comment.
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)
tlv/bench_test.go
Outdated
There was a problem hiding this comment.
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.
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.
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.
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
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