Skip to content

Add 5-party networking and PRF key setup to ampc-actor-utils - #142

Merged
marsenis merged 3 commits into
tfhard-5pcfrom
tfhard/claude/networking-prf
Sep 2, 2026
Merged

Add 5-party networking and PRF key setup to ampc-actor-utils#142
marsenis merged 3 commits into
tfhard-5pcfrom
tfhard/claude/networking-prf

Conversation

@marsenis

Copy link
Copy Markdown
Contributor

Generalizes the node-setup layer beyond the 3-party ring: NetworkSession gains Role-addressed send_to/receive_from, MpcNetworkHandle/PeerConnections now establish connections to any number of peers instead of exactly 2, and a new MeshControlChannel provides a Role-addressed control-plane channel (with an all-to-all sync barrier) alongside the untouched 3-party ControlChannel. Also fixes build_network_handle deriving only 3 local identities regardless of party count.

Adds two new shared-PRF key configurations for a 5-party protocol: ThresholdPrfKeys (a (3,5) config where k_{i,j} is known only to the 3 parties outside {i,j}) and PairwisePrfKeys (lambda_{i,j} known only to i and j), each with a symmetric XOR-combine network handshake in protocol::ops. All existing 3PC structs, signatures, and behavior are unchanged.

Generalizes the node-setup layer beyond the 3-party ring: NetworkSession
gains Role-addressed send_to/receive_from, MpcNetworkHandle/PeerConnections
now establish connections to any number of peers instead of exactly 2, and
a new MeshControlChannel provides a Role-addressed control-plane channel
(with an all-to-all sync barrier) alongside the untouched 3-party
ControlChannel. Also fixes build_network_handle deriving only 3 local
identities regardless of party count.

Adds two new shared-PRF key configurations for a 5-party protocol:
ThresholdPrfKeys (a (3,5) config where k_{i,j} is known only to the 3
parties outside {i,j}) and PairwisePrfKeys (lambda_{i,j} known only to i
and j), each with a symmetric XOR-combine network handshake in
protocol::ops. All existing 3PC structs, signatures, and behavior are
unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@evagelia evagelia left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Left a bunch of comments, in general looks good though!

Comment thread ampc-actor-utils/src/execution/local.rs Outdated
LOCAL_IDENTITY_NAMES[..n]
.iter()
.map(|name| Identity::from(*name))
.collect()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is there a reason for making this general instead of just adding 3 more names in the previous function? Seems also that LOCAL_IDENTITY_NAMES is only used in this function.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I guess the generality was not necessary. I'm changing it to having two custom functions. One for 3PC identities and one for ORBIT5 identities to make sure no one accidentally uses this for 4 parties or some other value they're not supposed to.

/// [`crate::network::mpc::NetworkHandle::mesh_control_channel`] again to
/// reconnect.
#[async_trait]
pub trait MeshControlChannel: Send {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could we potentially avoid duplication from adding this if we just add send_to(role) and recv_from(role) in the existing ControlChannel trait?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is what Claude had to say:

  • No hidden failures. Merging would mean a 5-party channel where send_next/recv_prev compile fine but always fail at runtime — exactly the kind of latent failure your engineering rules say to avoid. With separate traits, calling ring methods on a mesh channel is a compile error, not a production incident.
  • Downstream compatibility. ControlChannel is implemented outside this crate — iris-mpc-cpu's checkpoint protocol has its own InMemoryRing implementation of the trait in iris-mpc/iris-mpc-cpu/src/checkpoint_protocol/transport.rs. Adding required methods to ControlChannel breaks that implementation the moment iris-mpc bumps its pinned rev; adding them as defaulted methods that return errors just reintroduces the hidden-failure problem in every existing implementor.
  • The semantics genuinely differ, not just the arity. sync() on the ring is a 3-party barrier over two streams; on the mesh it's an all-to-all barrier over N−1 streams with deterministic role ordering. And "next"/"prev" have no canonical meaning in a mesh — a merged trait would force every caller to know which subset of the API is live for the object they hold.
  • The mesh trait already subsumes the ring. 5PC code addresses parties by Role via send/recv; there is nothing a merged trait would let it do that MeshControlChannel doesn't. The two traits partition cleanly: ring code holds a ControlChannel, N-party code holds a MeshControlChannel, and NetworkHandle hands out whichever one the caller asks for.

The 2nd and 3rd point convince me it's technically easier to keep the separate trait for now.

And this applies only to the control channel. This is different from the channels they use for data during the MPC protocol, which are non-blocking channels already in a mesh configuration.

Comment thread ampc-actor-utils/src/protocol/prf.rs Outdated

/// Number of parties in the 5-party protocol configuration used by
/// [`ThresholdPrfKeys`] and [`PairwisePrfKeys`].
pub const FIVE_PARTY_COUNT: u8 = 5;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

There's already a constant "pub const N_PARTIES: usize = 3;" in ampc-actor-utils/src/constants.rs, maybe we can use that to switch between 3 and 5 party versions?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It's tricky. If we use that to switch between a 3-party and a 5-party version, then unit testing becomes complicated because they'd need to override the constant. I was thinking that the switch from 3-parties to 5-parties will happen on the "front-end", the code on the DI repo that will eventually call the 5-party functions instead of the 3-party ones.
But I agree that the name FIVE_PARTY_COUNT is weird. I'll rename it to ORBIT5_PARTY_COUNT.

Comment thread ampc-actor-utils/src/protocol/ops.rs Outdated
for pair in PartyPair::excluding(own_role) {
let (a, b) = pair.parties();
let co_owners: Vec<Role> = (0..FIVE_PARTY_COUNT)
.map(|i| Role::new(i as usize))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: This is what the five_party_roles() in prf.rs does, maybe use that?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good point. Fixed all those instances.

Comment thread ampc-actor-utils/src/protocol/ops.rs Outdated

let mut seeds = BTreeMap::new();
for other in (0..FIVE_PARTY_COUNT)
.map(|i| Role::new(i as usize))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: same as comment above, maybe use five_party_roles() here?

}

let mut seeds = BTreeMap::new();
for pair in PartyPair::excluding(own_role) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think equivalent to this whole loop we can write smth like: (0..5).filter(|r| *r != own).array_combinations(); (from itertools).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah, but I think it's better abstraction to hide the implementation under the excluding method. We could apply this change inside excluding() but since it's working fine right now let's keep it this way.

marsenis and others added 2 commits September 1, 2026 15:06
ORBIT5 is the internal name of the 5-party system. Also makes the roles
helper public and replaces the manual (0..N).map(Role::new) constructions
in the PRF key setup functions and tests with it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Restores the original 3-party generate_local_identities() and adds
generate_local_identities_orbit5() with the five party names hardcoded,
instead of slicing a shared list by count. build_network_handle now
dispatches on the address count and rejects anything other than 3 or 5
parties explicitly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@marsenis marsenis left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the review, @evagelia

Comment thread ampc-actor-utils/src/protocol/prf.rs Outdated

/// Number of parties in the 5-party protocol configuration used by
/// [`ThresholdPrfKeys`] and [`PairwisePrfKeys`].
pub const FIVE_PARTY_COUNT: u8 = 5;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It's tricky. If we use that to switch between a 3-party and a 5-party version, then unit testing becomes complicated because they'd need to override the constant. I was thinking that the switch from 3-parties to 5-parties will happen on the "front-end", the code on the DI repo that will eventually call the 5-party functions instead of the 3-party ones.
But I agree that the name FIVE_PARTY_COUNT is weird. I'll rename it to ORBIT5_PARTY_COUNT.

}

let mut seeds = BTreeMap::new();
for pair in PartyPair::excluding(own_role) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah, but I think it's better abstraction to hide the implementation under the excluding method. We could apply this change inside excluding() but since it's working fine right now let's keep it this way.

Comment thread ampc-actor-utils/src/protocol/ops.rs Outdated
for pair in PartyPair::excluding(own_role) {
let (a, b) = pair.parties();
let co_owners: Vec<Role> = (0..FIVE_PARTY_COUNT)
.map(|i| Role::new(i as usize))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good point. Fixed all those instances.

/// [`crate::network::mpc::NetworkHandle::mesh_control_channel`] again to
/// reconnect.
#[async_trait]
pub trait MeshControlChannel: Send {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is what Claude had to say:

  • No hidden failures. Merging would mean a 5-party channel where send_next/recv_prev compile fine but always fail at runtime — exactly the kind of latent failure your engineering rules say to avoid. With separate traits, calling ring methods on a mesh channel is a compile error, not a production incident.
  • Downstream compatibility. ControlChannel is implemented outside this crate — iris-mpc-cpu's checkpoint protocol has its own InMemoryRing implementation of the trait in iris-mpc/iris-mpc-cpu/src/checkpoint_protocol/transport.rs. Adding required methods to ControlChannel breaks that implementation the moment iris-mpc bumps its pinned rev; adding them as defaulted methods that return errors just reintroduces the hidden-failure problem in every existing implementor.
  • The semantics genuinely differ, not just the arity. sync() on the ring is a 3-party barrier over two streams; on the mesh it's an all-to-all barrier over N−1 streams with deterministic role ordering. And "next"/"prev" have no canonical meaning in a mesh — a merged trait would force every caller to know which subset of the API is live for the object they hold.
  • The mesh trait already subsumes the ring. 5PC code addresses parties by Role via send/recv; there is nothing a merged trait would let it do that MeshControlChannel doesn't. The two traits partition cleanly: ring code holds a ControlChannel, N-party code holds a MeshControlChannel, and NetworkHandle hands out whichever one the caller asks for.

The 2nd and 3rd point convince me it's technically easier to keep the separate trait for now.

And this applies only to the control channel. This is different from the channels they use for data during the MPC protocol, which are non-blocking channels already in a mesh configuration.

Comment thread ampc-actor-utils/src/execution/local.rs Outdated
LOCAL_IDENTITY_NAMES[..n]
.iter()
.map(|name| Identity::from(*name))
.collect()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I guess the generality was not necessary. I'm changing it to having two custom functions. One for 3PC identities and one for ORBIT5 identities to make sure no one accidentally uses this for 4 parties or some other value they're not supposed to.

@gayathrigarimella gayathrigarimella left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

everything looks good to me!

@marsenis
marsenis merged commit 4a1fb29 into tfhard-5pc Sep 2, 2026
10 checks passed
@marsenis
marsenis deleted the tfhard/claude/networking-prf branch September 2, 2026 18:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants