Skip to content
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

Ping-pong topology fixes #281

Merged
merged 1 commit into from
Aug 30, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 103 additions & 27 deletions draft-irtf-cfrg-vdaf.md
Original file line number Diff line number Diff line change
Expand Up @@ -1237,9 +1237,33 @@ terminal states are `Rejected`, which indicates that the report cannot be
processed any further, and `Finished(out_share)`, which indicates that the
Aggregator has recovered an output share `out_share`.

~~~
class State:
pass

class Start(State):
pass

class Continued(State):
def __init__(self, prep_state):
self.prep_state = prep_state

class Finished(State):
def __init__(self, output_share):
self.output_share = output_share

class Rejected(State):
def __init__(self):
pass
~~~

Note that there is no representation of the `Start` state as it is never
instantiated in the ping-pong topology.
Comment on lines +1260 to +1261
Copy link
Collaborator

Choose a reason for hiding this comment

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

This seems confusing, as we just said "the initial state is Start". It is true that the Start state is ephemeral, and doesn't have to be stored anywhere, but I think it would be clearer to just have class Start(State): pass declared and move on. We're just defining which fields are held by which states here, and don't provide advice on their serialization.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You and Chris both think it makes more sense, and you're the editors, so I'll add it.


For convenience, the methods described in this section are defined in terms of
opaque byte strings. A compatible `Vdaf` MUST specify methods for encoding
public shares, input shares, prep shares, and prep messages. Minimally:
public shares, input shares, prep shares, prep messages, and aggregation
parameters. Minimally:

* `Vdaf.decode_public_share(encoded: bytes) -> Vdaf.PublicShare` decodes a
public share.
Expand All @@ -1262,6 +1286,12 @@ public shares, input shares, prep shares, and prep messages. Minimally:
Vdaf.PrepMessage` decodes a prep message, using the prep state as optional
decoding context.

cjpatton marked this conversation as resolved.
Show resolved Hide resolved
* `Vdaf.decode_agg_param(encoded: bytes) -> Vdaf.AggParam` decodes an
aggregation parameter.

* `Vdaf.encode_agg_param(agg_param: Vdaf.AggParam) -> bytes` encodes an
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should adjust the definition of encode_agg_param for Poplar1 in 8.2.6.6 to match this, so it takes in the aggregation parameter as a single tuple argument (rather than two separate arguments).

aggregation parameter.

Implementations of Prio3 and Poplar1 MUST use the encoding scheme specified in
{{prio3-encode}} and {{poplar1-encode}} respectively.

Expand Down Expand Up @@ -1322,13 +1352,24 @@ struct {
} Message;
~~~

These messages are used to transition between the states described in
{{vdaf-prep-comm}}. They are encoded and decoded to or from byte buffers as
described {{Section 3 of !RFC8446}}) using the following routines:

* `encode_ping_pong_message(message: Message) -> bytes` encodes a `Message` into
an opaque byte buffer.

* `decode_pong_pong_message(encoded: bytes) -> Message` decodes an opaque byte
buffer into a `Message`, raising an error if the bytes are not a valid
encoding.

The Leader's initial transition is computed with the following procedure:

~~~ transition
def ping_pong_leader_init(
Vdaf,
vdaf_verify_key: bytes[Vdaf.VERIFY_KEY_SIZE],
agg_param: Vdaf.AggParam,
agg_param: bytes,
nonce: bytes[Vdaf.NONCE_SIZE],
public_share: bytes,
input_share: bytes,
Expand All @@ -1337,44 +1378,47 @@ def ping_pong_leader_init(
(prep_state, prep_share) = Vdaf.prep_init(
vdaf_verify_key,
0,
agg_param,
Vdaf.decode_agg_param(agg_param),
nonce,
Vdaf.decode_public_share(public_share),
Vdaf.decode_input_share(0, input_share),
)
outbound = Message.initialize(
Vdaf.encode_prep_share(prep_share))
return (Continued(prep_state), outbound)
return (Continued(prep_state), encode_ping_pong_message(outbound))
except:
return (Rejected(), None)
~~~

If the Leader's state is `Rejected`, then processing halts. Otherwise, if the
state is `Continued`, then processing continues.
The output is the `State` to which the Leader has transitioned and an encoded
`Message`. If the Leader's state is `Rejected`, then processing halts.
Otherwise, if the state is `Continued`, then processing continues.

The Leaders sends the outbound message to the Helper. The Helper's initial
The Leader sends the outbound message to the Helper. The Helper's initial
transition is computed using the following procedure:

~~~ transition
def ping_pong_helper_init(
Vdaf,
vdaf_verify_key: bytes[Vdaf.VERIFY_KEY_SIZE],
agg_param: Vdaf.AggParam,
agg_param: bytes,
nonce: bytes[Vdaf.NONCE_SIZE],
public_share: bytes,
input_share: bytes,
inbound: Message,
inbound_encoded: bytes,
) -> tuple[State, bytes]:
try:
(prep_state, prep_share) = Vdaf.prep_init(
vdaf_verify_key,
1,
agg_param,
Vdaf.decode_agg_param(agg_param),
nonce,
Vdaf.decode_public_share(public_share),
Vdaf.decode_input_share(1, input_share),
)

inbound = decode_ping_pong_message(inbound_encoded)

if inbound.type != 0: # initialize
return (Rejected(), None)

Expand All @@ -1400,40 +1444,58 @@ def ping_pong_transition(
agg_param: Vdaf.AggParam,
prep_shares: list[Vdaf.PrepShare],
prep_state: Vdaf.PrepState,
) -> (State, Optional[Message]):
) -> (State, bytes):
prep_msg = Vdaf.prep_shares_to_prep(agg_param,
prep_shares)
out = Vdaf.prep_next(prep_state, prep_msg)
if type(out) == Vdaf.OutShare:
outbound = Message.finish(Vdaf.encode_prep_msg(prep_msg))
return (Finished(out), outbound)
return (Finished(out), encode_ping_pong_message(outbound))
(prep_state, prep_share) = out
outbound = Message.continue(
Vdaf.encode_prep_msg(prep_msg),
Vdaf.encdoe_prep_share(prep_share),
Vdaf.encode_prep_share(prep_share),
)
return (Continued(prep_state), outbound)
return (Continued(prep_state), encode_ping_pong_message(outbound))
~~~

If the Helper's state is `Finished` or `Rejected`, then processing halts.
Otherwise, if the state is `Continued`, then processing continues.
The output is the `State` to which the Helper has transitioned and an encoded
`Message`. If the Helper's state is `Finished` or `Rejected`, then processing
halts. Otherwise, if the state is `Continued`, then processing continues.

Next, the Helper sends the outbound message to the Leader. The Leader computes
its next state transition using the following algorithm, with `is_leader ==
True`:
its next state transition using the function `ping_pong_leader_continued`:

~~~ transition
def ping_pong_leader_continued(
Vdaf,
agg_param: bytes,
state: State,
inbound_encoded: bytes,
) -> (State, Optional[bytes]):
return Vdaf.ping_pong_continued(
True,
agg_param,
state,
inbound_encoded,
)

def ping_pong_continued(
Vdaf,
is_leader: bool,
agg_param: Vdaf.AggParam,
agg_param: bytes,
state: State,
inbound: Message,
) -> (State, Optional[Message]):
inbound_encoded: bytes,
) -> (State, Optional[bytes]):
try:
inbound = decode_ping_pong_message(inbound_encoded)

if inbound.type == 0: # initialize
return (Rejected(), None)

if !isinstance(state, Continued):
return (Rejected(), None)

prep_msg = Vdaf.decode_prep_msg(state.prep_state, inbound.prep_msg)
out = Vdaf.prep_next(state.prep_state, prep_msg)
if type(out) == tuple[Vdaf.PrepState, Vdaf.PrepShare] \
Expand All @@ -1447,9 +1509,8 @@ def ping_pong_continued(
if is_leader:
prep_shares.reverse()
return Vdaf.ping_pong_transition(
is_leader,
agg_param,
prep_shares
Vdaf.decode_agg_param(agg_param),
prep_shares,
prep_state,
)
elif type(out) == Vdaf.OutShare and inbound.type == 2:
Expand All @@ -1464,8 +1525,23 @@ def ping_pong_continued(

If the Leader's state is `Finished` or `Rejected`, then processing halts.
Otherwise, the Leader sends the outbound message to the Helper. The Helper
computes its next state transition by calling `ping_pong_continued()` with
`is_leader == False`.
computes its next state transition using the function
`ping_pong_helper_continued`:

~~~ transition
def ping_pong_helper_continued(
Vdaf,
agg_param: bytes,
state: State,
inbound_encoded: bytes,
) -> (State, Optional[bytes]):
return Vdaf.ping_pong_continued(
False,
agg_param,
state,
inbound_encoded,
)
~~~

They continue in this way until processing halts. Note that, depending on the
number of rounds of preparation that are required, there may be one more
Expand Down Expand Up @@ -3865,7 +3941,7 @@ The aggregation parameter is encoded as follows:
> Decide whether to RECOMMEND this encoding, and if so, add it to test vectors.

~~~
def encode_agg_param(Poplar1, level, prefixes):
def encode_agg_param(Poplar1, (level, prefixes)):
if level > 2 ** 16 - 1:
raise ERR_INPUT # level too deep
if len(prefixes) > 2 ** 32 - 1:
Expand Down