Skip to content
125 changes: 108 additions & 17 deletions draft-ietf-moq-transport.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,11 +243,6 @@ x (L):

: Indicates that x is L bits long

x (i):

: Indicates that x holds an integer value using the variable-length
encoding as described in ({{?RFC9000, Section 16}})

x (..):

: Indicates that x can be any length including zero bits long. Values
Expand All @@ -262,6 +257,13 @@ x (L) ...:
: Indicates that x is repeated zero or more times and that each instance
has a length of L

This document redfines the following RFC9000 syntax:

x (i):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we change (i) to avoid confusion with QUIC?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'm not sure it's a real issue to be confused with QUIC, since we define the encoding in this document, and have this comment pointing to the definition.

However, I'm open to suggestions. x (v) for varint? I like x (ï)

Copy link
Collaborator

Choose a reason for hiding this comment

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

I am also fine with x(i) as it is redefinition. If not, x(V) seems fine to me

Copy link
Contributor

Choose a reason for hiding this comment

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

I posted somewhere already but I can't find it, so I want to repeat it here. I hate the idea of redefinition, it provides no value and only a source of eternal confusion. Please just pick a different identifier like mi or whatever.

In isolation you think you have no issues. When I want to e.g. draw a diagram that renders how a MoQ control message is sent/received on an HTTP/3 stream I'm going to want to draw something like

STREAM Frame {
  Type (i) = 0x08..0x0f,
  Stream ID (i),
  [Offset (i)],
  [Length (i)],
  GOAWAY Message {
    Type (i) = 0x10,
    Length (16),
    New Session URI Length (i),
    New Session URI (..),
  }

see the ambiguity?

Copy link
Collaborator

Choose a reason for hiding this comment

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

+1 on trying to keep the syntax so it does not clash with quic

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I guess I'm sold by @LPardue's example.

Copy link
Collaborator

Choose a reason for hiding this comment

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

+1 ... how about we define x(V64) to be very explicit about the intent of the type ?

@LPardue @afrind @fluffy ??


: Indicates that x holds an integer value using the variable-length
encoding as described in {{variable-length-integers}}.

This document extends the RFC9000 syntax and with the additional field types:

x (b):
Expand All @@ -276,10 +278,47 @@ x (tuple):
as described in ({{?RFC9000, Section 16}}), followed by that many variable
length tuple fields, each of which are encoded as (b) above.


### Variable-Length Integers

MoQT requires a variable-length integer encoding with the following properties:

1. The encoded length can be determined from the first encoded byte.
2. The range of 1 byte values is as large as possible.
3. All 64 bit numbers can be encoded.

The variable-length integer encoding uses the most significant one to four
bits of the first byte to indicate the length of the encoding in bytes. The
remaining bits represent the integer value, encoded in network byte order.

Integers are encoded in 1, 2, 4, 8, or 9 bytes and can encode 7-, 14-, 29-, 60-,
or 64-bit values, respectively. The following table summarizes the encoding
properties.

|--------------|----------------|-------------|---------------|
| Leading Bits | Length (bytes) | Usable Bits | Range |
|--------------|----------------|-------------|---------------|
| 0 | 1 | 7 | 0-127 |
|--------------|----------------|-------------|---------------|
| 10 | 2 | 14 | 0-16,383 |
|--------------|----------------|-------------|---------------|
| 110 | 4 | 29 | 0-536,870,911 |
|--------------|----------------|-------------|---------------|
| 1110 | 8 | 60 | 0-2^60-1 |
|--------------|----------------|-------------|---------------|
| 1111XXXX | 9 | 64 | 0-2^64-1 |
Copy link
Collaborator

Choose a reason for hiding this comment

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

I have no idea what this actually means. What are the XXXX?

Do we expect to revisit this scheme later? If the goal is to make "the range of 1 byte values is as large as possible", it would make more sense to just allocate 0x00~0xf8 values for those, but then we basically don't have two-byte varints.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I have no idea what this actually means. What are the XXXX?

The 9 byte encoding uses 4 bits for length with 4 reserved bits.

Do we expect to revisit this scheme later?

No. If we make the switch let's go from QUIC to MoQ varints once and never look back. Now is the time to propose new schemes.

allocate 0x00~0xf8 values for those, but then we basically don't have two-byte varints.

To clarify, you if the first byte is > 0xf8 the length is value - 0xf8 + 1? Technically you have 2 byte varints but they only encode 7 values that can't be encoded in 1 byte. These things are all about tradeoffs and I'm agnostic if the group prefers 248 1-byte values, at the expense of more bytes for larger values. It might be bad for shortish payload lengths (eg: 300-1200 bytes, though datagrams don't have a length field).

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think the "1111XXXX" should be "11110000" and then we should have
"111110001 to 11111111" as RESERVED

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@fluffy sounds good to me. Would you have it be an error to receive 11110001-11111111? I don't want to check those bits in my decoder.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't care if the receiver never checks them, I just care that senders set them all zero and that future extensions can negotiate using them.

That said, it really does not seem a big deal to check and it's going to be easier to debug if you get a great reason phrase explaining what is going on when they are not valid.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I pulled up short of making 11110001-11111111 an error and backtracked on reserved, just saying ignored/SHOULD. Since it's a 64 bit value, there's no use for these bits. My thinking was that I didn't want any way processing the first byte could fail - but then the coffee kicked in a bit more and I realized decoding can always fail on some subsequent byte (running out of input) so I don't know that this is a huge advantage.

I'm open to doing whatever here, but want a broader signal from the wg. We should also discuss if we like Victor's idea better.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Still want the change to 1111XXXX to 11110000 and leave the other values as reserved for future extensions.

|--------------|----------------|-------------|---------------|
{: format title="Summary of Integer Encodings"}

The four least significant bits of the first byte are reserved in 9-byte
encodings.

To reduce unnecessary use of bandwidth, variable length integers SHOULD
be encoded using the least number of bytes possible to represent the
required value.

A sample encoding and decoding algorithm is in {{sample-varint}}.

### Location Structure

Location identifies a particular Object in a Group within a Track.
Expand Down Expand Up @@ -2060,18 +2099,18 @@ SUBSCRIBE_DONE Message {
* Status Code: An integer status code indicating why the subscription ended.

* Stream Count: An integer indicating the number of data streams the publisher
opened for this subscription. This helps the subscriber know if it has received
all of the data published in this subscription by comparing the number of
streams received. The subscriber can immediately remove all subscription state
once the same number of streams have been processed. If the track had
Forwarding Preference = Datagram, the publisher MUST set Stream Count to 0. If
the publisher is unable to set Stream Count to the exact number of streams
opened for the subscription, it MUST set Stream Count to 2^62 - 1. Subscribers
SHOULD use a timeout or other mechanism to remove subscription state in case
the publisher set an incorrect value, reset a stream before the SUBGROUP_HEADER,
or set the maximum value. If a subscriber receives more streams for a
subscription than specified in Stream Count, it MAY close the session with a
Protocol Violation.
opened for this subscription. This helps the subscriber know if it has
received all of the data published in this subscription by comparing the
number of streams received. The subscriber can immediately remove all
subscription state once the same number of streams have been processed. If
the track had Forwarding Preference = Datagram, the publisher MUST set Stream
Count to 0. If the publisher is unable to set Stream Count to the exact
number of streams opened for the subscription, it MUST set Stream Count to a
value greater than or equal to 2^60. Subscribers SHOULD use a timeout or other
mechanism to remove subscription state in case the publisher set an incorrect
value, reset a stream before the SUBGROUP_HEADER, or set the maximum value. If
a subscriber receives more streams for a subscription than specified in Stream
Count, it MAY close the session with a Protocol Violation.

* Error Reason: Provides the reason for subscription error. See {{reason-phrase}}.

Expand Down Expand Up @@ -3443,6 +3482,58 @@ document:

--- back

# Sample Variable-Length Integer Encoding and Decoding {#sample-varint}

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm against adding example code like this to the draft - it just makes the draft longer and tends to accumulate errors over time. It gets endless review comments on better ways to write it. People can figure out how to write this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'm mirroring RFC 9000 here, but I don't have a strong opinion.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I tend to think it's somewhat useful.

Copy link
Collaborator

Choose a reason for hiding this comment

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

This is in an appendix, correct?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yes, this in an appendix

The WriteVarint function takes two parameters, a 64 bit integer and an
output buffer with an append operation.

~~~pseudocode
Copy link
Collaborator

Choose a reason for hiding this comment

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

Im very against putting code like this in this draft. I'm fine with another draft about implementation advice but we need to keep this draft as short as possible.

Function WriteVarint(value, output):
if value <= 127:
output.append(value, 1)
else if value <= 16383:
output.append(networkByteOrder(uint16_t(value | 0x8000)), 2)
else if value <= 536870911:
output.append(networkByteOrder(
uint32_t(value | 0xC0000000)), 4)
else if value <= 1152921504606846975:
output.append(networkByteOrder(
uint64_t(value | 0xE000000000000000)), 8)
else:
output.append(0xF0, 1)
output.append(networkByteOrder(value), 8)
~~~


The function ReadVarint takes a single argument -- a sequence of bytes, which
can be read in network byte order.

~~~pseudocode
ReadVarint(data):
lengths = [ 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 8, 9 ]
masks = [ 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f,
0x3f, 0x3f, 0x3f, 0x3f, 0x1f, 0x1f, 0x0f, 0x00 ]
// The length of variable-length integers is encoded in the
// first one to four bits of the first byte.
v = data.next_byte()
prefix = v >> 4
length = lengths[prefix]
mask = masks[prefix]

// Once the length is known, remove these bits and read any
// remaining bytes.
v = v & mask
repeat length-1 times:
v = (v << 8) + data.next_byte()
return v
~~~
For example, the nine-byte sequence 0xf0ffffffffffffffff decodes to the decimal
value 18,446,744,073,709,551,615; the eight-byte sequence 0xe2197c5eff14e88c
decodes to the decimal value 151,288,809,941,952,652; the four-byte sequence
0xdd7f3e7d decodes to 494,878,333; the two-byte sequence 0xbbbd decodes to
15,293; and the single byte 0x25 decodes to 37 (as does the two-byte sequence
0x8025).

# Change Log

RFC Editor's Note: Please remove this section prior to publication of a final version of this document.
Expand Down