This PR is a significant refactor and breaking change to the tofn SDK implementer API. The crate API surface and binary message format have changed slightly but these changes should not break existing downstream code.
How it was
Previously we required each round to specify at compile time the combination of bcast and/or p2ps messages it expects to receive from other parties. A consequence is that each round must expect that combo of message types regardless of whether it's in happy/sad path. This restriction is a problem because some rounds might, for example, require bcast-only in happy path but p2ps-only in sad path. Previously the only workaround is to send empty dummy bcast messages and/or bundle all p2ps into a single bcast, both of which are Very Bad™.
An additional annoyance with the previous design is copied code across different modules no_messages, bcast_only, p2ps_only, bcast_and_p2ps.
How it is now
There is now a single Executer trait that all rounds must implement. The new executer method has the following argument changes:
bcasts_in: FillVecMap: bcasts_in.get(from) is None if share from declined to send a bcast. Here we use the now-familiar FillVecMap.
p2ps_in: P2ps: p2ps_in.get(from) is None if share from declined to send p2ps. Here I've modified P2ps to be a VecMap of Option<HoleVecMap>. Each share must either send all p2ps or none of them---the SDK will abort the protocol with faulters if some but not all p2ps are received from a share.
It is now the job of the protocol implementer to check in each round that the needed messages are present. (The previous SDK design sought to avoid this boilerplate.)
Each share declares its expected message types
How does the SDK know which types of messages to expect from a given share in a given round? ie. How does expecting_more_msgs_this_round() work? Due to nondeterministic delivery of messages, we cannot simply deduce the answer from observation. (eg. If I receive only a bcast from party X does that mean that party X intends to send only bcasts this round, or is X also sending p2ps and I just haven't received any yet?)
When share X ends a round it must specify:
bcast_out: Option<BytesVec>
p2ps_out: Option<HoleVecMap<K, BytesVec>>
From this the SDK auto-magically deduces X's intentions and bundles a declaration into the binary payload of every outgoing message from X. On the receiving side, share Y learns what types of messages to expect from X after receiving any message from X. If X sends inconsistent declarations then X is declared as a faulter by Y.
An annoying special case is total_share_count == 1 and p2ps-only. In this case the party will send zero messages and so the SDK cannot learn what to expect. I've left this special case as a TODO for the future. My suggested solution is to send an empty dummy bcast message with a special new declaration to be used only in this special case. This declaration says, "This is a p2ps-only round so you should not see any messages from me."
Changes to P2ps
As mentioned above, P2ps is now a VecMap of Option<HoleVecMap>. What used to be P2ps is now called FullP2ps. FullP2ps is still used in happy path cases where we want to remember the p2ps we received from previous rounds. To conclude: there are now 3 p2ps-related structs:
FillP2ps as before
P2ps is new
FullP2ps formerly P2ps
I added methods to convert among these structs. The hierarchy is FillP2ps > P2ps > FullP2ps where x > y indicates that x can be converted to y.
I expect protocol implementers should have no use for FillP2ps. Current module hierarchy means we have no way to restrict visibility of collection types. We can discuss this if there's concern.
This PR is a significant refactor and breaking change to the tofn SDK implementer API. The crate API surface and binary message format have changed slightly but these changes should not break existing downstream code.
How it was
Previously we required each round to specify at compile time the combination of bcast and/or p2ps messages it expects to receive from other parties. A consequence is that each round must expect that combo of message types regardless of whether it's in happy/sad path. This restriction is a problem because some rounds might, for example, require bcast-only in happy path but p2ps-only in sad path. Previously the only workaround is to send empty dummy bcast messages and/or bundle all p2ps into a single bcast, both of which are Very Bad™.
An additional annoyance with the previous design is copied code across different modules
no_messages
,bcast_only
,p2ps_only
,bcast_and_p2ps
.How it is now
There is now a single
Executer
trait that all rounds must implement. The newexecuter
method has the following argument changes:bcasts_in: FillVecMap
:bcasts_in.get(from)
isNone
if sharefrom
declined to send a bcast. Here we use the now-familiarFillVecMap
.p2ps_in: P2ps
:p2ps_in.get(from)
isNone
if sharefrom
declined to send p2ps. Here I've modifiedP2ps
to be aVecMap
ofOption<HoleVecMap>
. Each share must either send all p2ps or none of them---the SDK will abort the protocol with faulters if some but not all p2ps are received from a share.It is now the job of the protocol implementer to check in each round that the needed messages are present. (The previous SDK design sought to avoid this boilerplate.)
Each share declares its expected message types
How does the SDK know which types of messages to expect from a given share in a given round? ie. How does
expecting_more_msgs_this_round()
work? Due to nondeterministic delivery of messages, we cannot simply deduce the answer from observation. (eg. If I receive only a bcast from party X does that mean that party X intends to send only bcasts this round, or is X also sending p2ps and I just haven't received any yet?)When share X ends a round it must specify:
bcast_out: Option<BytesVec>
p2ps_out: Option<HoleVecMap<K, BytesVec>>
From this the SDK auto-magically deduces X's intentions and bundles a declaration into the binary payload of every outgoing message from X. On the receiving side, share Y learns what types of messages to expect from X after receiving any message from X. If X sends inconsistent declarations then X is declared as a faulter by Y.
An annoying special case is
total_share_count == 1
and p2ps-only. In this case the party will send zero messages and so the SDK cannot learn what to expect. I've left this special case as a TODO for the future. My suggested solution is to send an empty dummy bcast message with a special new declaration to be used only in this special case. This declaration says, "This is a p2ps-only round so you should not see any messages from me."Changes to
P2ps
As mentioned above,
P2ps
is now aVecMap
ofOption<HoleVecMap>
. What used to beP2ps
is now calledFullP2ps
.FullP2ps
is still used in happy path cases where we want to remember the p2ps we received from previous rounds. To conclude: there are now 3 p2ps-related structs:FillP2ps
as beforeP2ps
is newFullP2ps
formerlyP2ps
I added methods to convert among these structs. The hierarchy is
FillP2ps > P2ps > FullP2ps
wherex > y
indicates thatx
can be converted toy
.I expect protocol implementers should have no use for
FillP2ps
. Current module hierarchy means we have no way to restrict visibility of collection types. We can discuss this if there's concern.