Skip to content

Commit 60a2703

Browse files
committed
sphinx: convert to variable sized EOB payloads
In this commit, we make the jump from the prior fixed sized frame payloads, to the latest variable sized format. The main change is that now we no longer need to pack all the payloads into a fixed frame size, adding padding. Instead, for the new TLV format, we'll now use a var-int to signal the length of the payload. In the process, we lift the Realm field from the HopPayload struct into the HopData struct. We do this as the new TLV payload has no realm field, and it's instead used to signal the existence of the legacy payload format.
1 parent 1ce3fd7 commit 60a2703

File tree

5 files changed

+163
-197
lines changed

5 files changed

+163
-197
lines changed

bench_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func BenchmarkPathPacketConstruction(b *testing.B) {
3333
}
3434
copy(hopData.NextAddress[:], bytes.Repeat([]byte{byte(i)}, 8))
3535

36-
hopPayload, err := NewHopPayload(0, &hopData, nil)
36+
hopPayload, err := NewHopPayload(&hopData, nil)
3737
if err != nil {
3838
b.Fatalf("unable to create new hop payload: %v", err)
3939
}

path.go

Lines changed: 135 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,14 @@
11
package sphinx
22

33
import (
4+
"bufio"
45
"bytes"
56
"encoding/binary"
67
"fmt"
78
"io"
8-
"math"
99

1010
"github.com/btcsuite/btcd/btcec"
11-
)
12-
13-
const (
14-
// RealmMaskBytes is the mask to apply the realm in order to pack or
15-
// decode the 4 LSB of the realm field.
16-
RealmMaskBytes = 0x0f
17-
18-
// NumFramesShift is the number of bytes to shift the encoding of the
19-
// number of frames by in order to pack/unpack them into the 4 MSB bits
20-
// of the realm field.
21-
NumFramesShift = 4
11+
"github.com/btcsuite/btcd/wire"
2212
)
2313

2414
// HopData is the information destined for individual hops. It is a fixed size
@@ -28,6 +18,10 @@ const (
2818
// hop, or zero if this is the packet is not to be forwarded, since this is the
2919
// last hop.
3020
type HopData struct {
21+
// Realm denotes the "real" of target chain of the next hop. For
22+
// bitcoin, this value will be 0x00.
23+
Realm [RealmByteSize]byte
24+
3125
// NextAddress is the address of the next hop that this packet should
3226
// be forward to.
3327
NextAddress [AddressSize]byte
@@ -54,6 +48,10 @@ type HopData struct {
5448
// Encode writes the serialized version of the target HopData into the passed
5549
// io.Writer.
5650
func (hd *HopData) Encode(w io.Writer) error {
51+
if _, err := w.Write(hd.Realm[:]); err != nil {
52+
return err
53+
}
54+
5755
if _, err := w.Write(hd.NextAddress[:]); err != nil {
5856
return err
5957
}
@@ -76,6 +74,10 @@ func (hd *HopData) Encode(w io.Writer) error {
7674
// Decodes populates the target HopData with the contents of a serialized
7775
// HopData packed into the passed io.Reader.
7876
func (hd *HopData) Decode(r io.Reader) error {
77+
if _, err := io.ReadFull(r, hd.Realm[:]); err != nil {
78+
return err
79+
}
80+
7981
if _, err := io.ReadFull(r, hd.NextAddress[:]); err != nil {
8082
return err
8183
}
@@ -90,23 +92,34 @@ func (hd *HopData) Decode(r io.Reader) error {
9092
return err
9193
}
9294

93-
if _, err := io.ReadFull(r, hd.ExtraBytes[:]); err != nil {
94-
return err
95-
}
96-
95+
_, err = io.ReadFull(r, hd.ExtraBytes[:])
9796
return err
9897
}
9998

99+
// PayloadType denotes the type of the payload included in the onion packet.
100+
// Serialization of a raw HopPayload will depend on the payload type, as some
101+
// include a varint length prefix, while others just encode the raw payload.
102+
type PayloadType uint8
103+
104+
const (
105+
// PayloadLegacy is the legacy payload type. It includes a fixed 32
106+
// bytes, 12 of which are padding, and uses a "zero length" (the old
107+
// realm) prefix.
108+
PayloadLegacy PayloadType = iota
109+
110+
// PayloadTLV is the new modern TLV based format. This payload includes
111+
// a set of opaque bytes with a varint length prefix. The varint used
112+
// is the same CompactInt as used in the Bitcoin protocol.
113+
PayloadTLV
114+
)
115+
100116
// HopPayload is a slice of bytes and associated payload-type that are destined
101117
// for a specific hop in the PaymentPath. The payload itself is treated as an
102-
// opaque data field by the onion router, while the Realm is modified to
103-
// indicate how many hops are to be read by the processing node. The 4 MSB in
104-
// the realm indicate how many additional hops are to be processed to collect
105-
// the entire payload.
118+
// opaque data field by the onion router. The included Type field informs the
119+
// serialization/deserialziation of the raw payload.
106120
type HopPayload struct {
107-
// realm denotes the "real" of target chain of the next hop. For
108-
// bitcoin, this value will be 0x00.
109-
realm [RealmByteSize]byte
121+
// Type is the type of the payload.
122+
Type PayloadType
110123

111124
// Payload is the raw bytes of the per-hop payload for this hop.
112125
// Depending on the realm, this pay be the regular legacy hop data, or
@@ -122,18 +135,22 @@ type HopPayload struct {
122135
// instructions for a hop, and a set of optional opaque extra onion bytes to
123136
// drop off at the target hop. If both values are not specified, then an error
124137
// is returned.
125-
func NewHopPayload(realm byte, hopData *HopData, eob []byte) (HopPayload, error) {
126-
138+
func NewHopPayload(hopData *HopData, eob []byte) (HopPayload, error) {
127139
var (
128140
h HopPayload
129141
b bytes.Buffer
130142
)
131143

132144
// We can't proceed if neither the hop data or the EOB has been
133145
// specified by the caller.
134-
if hopData == nil && len(eob) == 0 {
146+
switch {
147+
case hopData == nil && len(eob) == 0:
135148
return h, fmt.Errorf("either hop data or eob must " +
136149
"be specified")
150+
151+
case hopData != nil && len(eob) > 0:
152+
return h, fmt.Errorf("cannot provide both hop data AND an eob")
153+
137154
}
138155

139156
// If the hop data is specified, then we'll write that now, as it
@@ -142,90 +159,67 @@ func NewHopPayload(realm byte, hopData *HopData, eob []byte) (HopPayload, error)
142159
if err := hopData.Encode(&b); err != nil {
143160
return h, nil
144161
}
145-
}
146162

147-
// Finally, we'll write out the EOB portion to the same buffer to
148-
// ensure it comes after mandatory hop payload.
149-
if _, err := b.Write(eob); err != nil {
150-
return h, nil
163+
// We'll also mark that this particular hop will be using the
164+
// legacy format as the modern format packs the existing hop
165+
// data information into the EOB space as a TLV stream.
166+
h.Type = PayloadLegacy
167+
} else {
168+
// Otherwise, we'll write out the raw EOB which contains a set
169+
// of opaque bytes that the recipient can decode to make a
170+
// forwarding decision.
171+
if _, err := b.Write(eob); err != nil {
172+
return h, nil
173+
}
174+
175+
h.Type = PayloadTLV
151176
}
152177

153-
h.realm = [RealmByteSize]byte{realm}
154178
h.Payload = b.Bytes()
155179

156180
return h, nil
157181
}
158182

159-
// Realm returns the context specific representation of the realm for a hop.
160-
func (hp *HopPayload) Realm() byte {
161-
return hp.realm[0] & RealmMaskBytes
162-
}
163-
164-
// payloadRealm returns the realm that will be used within the raw packed hop
165-
// payload. This differs from the Realm method above in that it uses space to
166-
// encode the packet type and number of frames. The final encoding uses the
167-
// first 4 bits of the realm to encode the number of frames used, and the
168-
// latter 4 bits to encode the real realm type.
169-
func (hp *HopPayload) payloadRealm() byte {
170-
maskedRealm := hp.realm[0] & 0x0F
171-
numFrames := hp.NumFrames()
172-
173-
return maskedRealm | (byte(numFrames-1) << NumFramesShift)
174-
}
175-
176-
// NumFrames returns the total number of frames it'll take to pack the target
177-
// HopPayload into a Sphinx packet.
178-
func (hp *HopPayload) NumFrames() int {
179-
// If it all fits in the legacy payload size, don't use any additional
180-
// frames.
181-
if len(hp.Payload) <= 32 {
182-
return 1
183-
}
184-
185-
// Otherwise we'll need at least one additional frame: subtract the 64
186-
// bytes we can stuff into payload and hmac of the first, and the 33
187-
// bytes we can pack into the payload of the second, then divide the
188-
// remainder by 65.
189-
remainder := len(hp.Payload) - 64 - 33
190-
return 2 + int(math.Ceil(float64(remainder)/65))
191-
}
192-
193-
// packRealm writes out the proper realm encoding in place to the target
194-
// io.Writer.
195-
func (hp *HopPayload) packRealm(w io.Writer) error {
196-
realm := hp.payloadRealm()
197-
if _, err := w.Write([]byte{realm}); err != nil {
198-
return err
183+
// NumBytes returns the number of bytes it will take to serialize the full
184+
// payload. Depending on the payload type, this may include some additional
185+
// signalling bytes.
186+
func (hp *HopPayload) NumBytes() int {
187+
// The base size is the size of the raw payload, and the size of the
188+
// HMAC.
189+
size := len(hp.Payload) + HMACSize
190+
191+
// If this is the new TLV format, then we'll also accumulate the number
192+
// of bytes that it would take to encode the size of the payload.
193+
if hp.Type == PayloadTLV {
194+
payloadSize := len(hp.Payload)
195+
size += int(wire.VarIntSerializeSize(uint64(payloadSize)))
199196
}
200197

201-
return nil
198+
return size
202199
}
203200

204201
// Encode encodes the hop payload into the passed writer.
205202
func (hp *HopPayload) Encode(w io.Writer) error {
206-
// We'll need to add enough padding bytes to position the HMAC at the
207-
// end of the payload
208-
padding := hp.NumFrames()*FrameSize - len(hp.Payload) - 1 - HMACSize
209-
if padding < 0 {
210-
return fmt.Errorf("cannot have negative padding: %v", padding)
211-
}
203+
switch hp.Type {
212204

213-
if err := hp.packRealm(w); err != nil {
214-
return err
215-
}
216-
if _, err := w.Write(hp.Payload); err != nil {
217-
return err
218-
}
205+
// For the legacy payload, we don't need to add any additional bytes as
206+
// our realm byte serves as our zero prefix byte.
207+
case PayloadLegacy:
208+
break
219209

220-
// If we need to pad out the frame at all, then we'll do so now before
221-
// we write out the HMAC.
222-
if padding > 0 {
223-
_, err := w.Write(bytes.Repeat([]byte{0x00}, padding))
210+
// For the TLV payload, we'll first prepend the length of the payload
211+
// as a var-int.
212+
case PayloadTLV:
213+
err := wire.WriteVarInt(w, 0, uint64(len(hp.Payload)))
224214
if err != nil {
225215
return err
226216
}
227217
}
228218

219+
// Finally, we'll write out the raw payload, then the HMAC in series.
220+
if _, err := w.Write(hp.Payload); err != nil {
221+
return err
222+
}
229223
if _, err := w.Write(hp.HMAC[:]); err != nil {
230224
return err
231225
}
@@ -236,19 +230,48 @@ func (hp *HopPayload) Encode(w io.Writer) error {
236230
// Decode unpacks an encoded HopPayload from the passed reader into the target
237231
// HopPayload.
238232
func (hp *HopPayload) Decode(r io.Reader) error {
239-
if _, err := io.ReadFull(r, hp.realm[:]); err != nil {
233+
bufReader := bufio.NewReader(r)
234+
235+
// In order to properly parse the payload, we'll need to check the
236+
// first byte. We'll use a bufio reader to peek at it without consuming
237+
// it from the buffer.
238+
peekByte, err := bufReader.Peek(1)
239+
if err != nil {
240240
return err
241241
}
242242

243-
numFrames := int(hp.realm[0]>>NumFramesShift) + 1
244-
numBytes := (numFrames * FrameSize) - 32 - 1
243+
var payloadSize uint32
244+
245+
switch int(peekByte[0]) {
246+
// If the first byte is a zero (the realm), then this is the normal
247+
// payload.
248+
case 0x00:
249+
// Our size is just the payload, without the HMAC. This means
250+
// that this is the legacy payload type.
251+
payloadSize = HopDataSize - HMACSize
252+
hp.Type = PayloadLegacy
253+
254+
default:
255+
// Otherwise, this is the new TLV based payload type, so we'll
256+
// extract the payload length encoded as a var-int.
257+
varInt, err := wire.ReadVarInt(bufReader, 0)
258+
if err != nil {
259+
return err
260+
}
245261

246-
hp.Payload = make([]byte, numBytes)
247-
if _, err := io.ReadFull(r, hp.Payload[:]); err != nil {
248-
return err
262+
payloadSize = uint32(varInt)
263+
hp.Type = PayloadTLV
249264
}
250265

251-
if _, err := io.ReadFull(r, hp.HMAC[:]); err != nil {
266+
// Now that we know the payload size, we'll create a new buffer to
267+
// read it out in full.
268+
//
269+
// TODO(roasbeef): can avoid all these copies
270+
hp.Payload = make([]byte, payloadSize)
271+
if _, err := io.ReadFull(bufReader, hp.Payload[:]); err != nil {
272+
return err
273+
}
274+
if _, err := io.ReadFull(bufReader, hp.HMAC[:]); err != nil {
252275
return err
253276
}
254277

@@ -260,31 +283,29 @@ func (hp *HopPayload) Decode(r io.Reader) error {
260283
// This method also returns the left over EOB that remain after the hop data
261284
// has been parsed. Callers may want to map this blob into something more
262285
// concrete.
263-
func (hp *HopPayload) HopData() (*HopData, []byte, error) {
286+
func (hp *HopPayload) HopData() (*HopData, error) {
287+
payloadReader := bytes.NewBuffer(hp.Payload)
288+
264289
// If this isn't the "base" realm, then we can't extract the expected
265290
// hop payload structure from the payload.
266-
if hp.Realm() != 0x00 {
267-
return nil, nil, fmt.Errorf("payload is not a HopData "+
268-
"payload, realm=%d", hp.Realm())
291+
if hp.Type != PayloadLegacy {
292+
return nil, nil
269293
}
270294

271295
// Now that we know the payload has the structure we expect, we'll
272296
// decode the payload into the HopData.
273297
var hd HopData
274-
payloadReader := bytes.NewBuffer(hp.Payload)
275298
if err := hd.Decode(payloadReader); err != nil {
276-
return &hd, nil, nil
299+
return nil, err
277300
}
278301

279-
// What's left over in the buffer that wasn't parsed as part of the
280-
// forwarding instructions is our lingering EOB.
281-
eob := payloadReader.Bytes()
282-
283-
return &hd, eob, nil
302+
return &hd, nil
284303
}
285304

286305
// NumMaxHops is the maximum path length. This should be set to an estimate of
287306
// the upper limit of the diameter of the node graph.
307+
//
308+
// TODO(roasbeef): adjust due to var-payloads?
288309
const NumMaxHops = 20
289310

290311
// PaymentPath represents a series of hops within the Lightning Network
@@ -352,17 +373,17 @@ func (p *PaymentPath) TrueRouteLength() int {
352373
return routeLength
353374
}
354375

355-
// TotalFrames returns the total numebr of frames that it'll take to create a
356-
// Sphinx packet from the target PaymentPath.
357-
func (p *PaymentPath) TotalFrames() int {
358-
var frameCount int
376+
// TotalPayloadSize returns the sum of the size of each payload in the "true"
377+
// route.
378+
func (p *PaymentPath) TotalPayloadSize() int {
379+
var totalSize int
359380
for _, hop := range p {
360381
if hop.IsEmpty() {
361-
break
382+
continue
362383
}
363384

364-
frameCount += hop.HopPayload.NumFrames()
385+
totalSize += hop.HopPayload.NumBytes()
365386
}
366387

367-
return frameCount
388+
return totalSize
368389
}

0 commit comments

Comments
 (0)